From 456b7c9e60e32a6b9e2832e4fe65d192a6d198f9 Mon Sep 17 00:00:00 2001 From: Johannes Wiest Date: Tue, 22 Oct 2024 16:17:52 +0200 Subject: [PATCH 01/18] Adaptive learning: Redesign edit competency relation section (#9447) --- .../UpdateCourseCompetencyRelationDTO.java | 9 + .../service/competency/CompetencyService.java | 5 +- .../competency/CourseCompetencyService.java | 29 +- .../competency/PrerequisiteService.java | 5 +- .../atlas/web/CourseCompetencyResource.java | 20 + .../competency-management-table.component.ts | 18 +- .../competency-management.component.html | 12 +- .../competency-management.component.ts | 104 ++--- .../competency-relation-graph.component.html | 133 ------- .../competency-relation-graph.component.scss | 28 -- .../competency-relation-graph.component.ts | 319 --------------- .../course/competencies/competency.module.ts | 4 - ...competencies-relation-graph.component.html | 36 ++ ...competencies-relation-graph.component.scss | 14 + ...e-competencies-relation-graph.component.ts | 80 ++++ ...competencies-relation-modal.component.html | 33 ++ ...competencies-relation-modal.component.scss | 5 + ...e-competencies-relation-modal.component.ts | 57 +++ ...se-competency-relation-form.component.html | 70 ++++ ...se-competency-relation-form.component.scss | 4 + ...urse-competency-relation-form.component.ts | 306 +++++++++++++++ ...se-competency-relation-node.component.html | 17 + ...se-competency-relation-node.component.scss | 20 + ...urse-competency-relation-node.component.ts | 38 ++ .../services/course-competency-api.service.ts | 36 +- .../webapp/app/entities/competency.model.ts | 4 + src/main/webapp/i18n/de/competency.json | 35 +- src/main/webapp/i18n/en/competency.json | 35 +- .../CourseCompetencyIntegrationTest.java | 26 ++ ...petency-management-table.component.spec.ts | 3 - .../competency-management.component.spec.ts | 83 +--- ...ompetency-relation-graph-stub.component.ts | 14 - ...ompetency-relation-graph.component.spec.ts | 193 ---------- ...petencies-relation-graph.component.spec.ts | 93 +++++ ...petencies-relation-modal.component.spec.ts | 129 +++++++ ...competency-relation-form.component.spec.ts | 364 ++++++++++++++++++ ...competency-relation-node.component.spec.ts | 50 +++ ...ourse-competencies-modal.component.spec.ts | 4 +- ...se-competencies-settings.component.spec.ts | 2 +- .../course-competency-api.service.spec.ts | 14 +- 40 files changed, 1546 insertions(+), 905 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/atlas/dto/UpdateCourseCompetencyRelationDTO.java delete mode 100644 src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.html delete mode 100644 src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.scss delete mode 100644 src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.ts create mode 100644 src/main/webapp/app/course/competencies/components/course-competencies-relation-graph/course-competencies-relation-graph.component.html create mode 100644 src/main/webapp/app/course/competencies/components/course-competencies-relation-graph/course-competencies-relation-graph.component.scss create mode 100644 src/main/webapp/app/course/competencies/components/course-competencies-relation-graph/course-competencies-relation-graph.component.ts create mode 100644 src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.html create mode 100644 src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.scss create mode 100644 src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.ts create mode 100644 src/main/webapp/app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component.html create mode 100644 src/main/webapp/app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component.scss create mode 100644 src/main/webapp/app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component.ts create mode 100644 src/main/webapp/app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component.html create mode 100644 src/main/webapp/app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component.scss create mode 100644 src/main/webapp/app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component.ts delete mode 100644 src/test/javascript/spec/component/competencies/competency-management/competency-relation-graph-stub.component.ts delete mode 100644 src/test/javascript/spec/component/competencies/competency-management/competency-relation-graph.component.spec.ts create mode 100644 src/test/javascript/spec/component/competencies/components/course-competencies-relation-graph.component.spec.ts create mode 100644 src/test/javascript/spec/component/competencies/components/course-competencies-relation-modal.component.spec.ts create mode 100644 src/test/javascript/spec/component/competencies/components/course-competency-relation-form.component.spec.ts create mode 100644 src/test/javascript/spec/component/competencies/components/course-competency-relation-node.component.spec.ts rename src/test/javascript/spec/component/competencies/components/{import-course-competencies-modal => }/import-all-course-competencies-modal.component.spec.ts (96%) rename src/test/javascript/spec/component/competencies/components/{import-course-competencies-settings => }/import-course-competencies-settings.component.spec.ts (98%) diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/dto/UpdateCourseCompetencyRelationDTO.java b/src/main/java/de/tum/cit/aet/artemis/atlas/dto/UpdateCourseCompetencyRelationDTO.java new file mode 100644 index 000000000000..d1bac2f5b3ca --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/dto/UpdateCourseCompetencyRelationDTO.java @@ -0,0 +1,9 @@ +package de.tum.cit.aet.artemis.atlas.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.atlas.domain.competency.RelationType; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record UpdateCourseCompetencyRelationDTO(RelationType newRelationType) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java index 9ec942846bf8..fbe46aa979b0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CompetencyService.java @@ -23,6 +23,7 @@ import de.tum.cit.aet.artemis.atlas.service.LearningObjectImportService; import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathService; import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.repository.CourseRepository; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.exercise.service.ExerciseService; import de.tum.cit.aet.artemis.lecture.repository.LectureUnitCompletionRepository; @@ -41,9 +42,9 @@ public CompetencyService(CompetencyRepository competencyRepository, Authorizatio LearningPathService learningPathService, CompetencyProgressService competencyProgressService, LectureUnitService lectureUnitService, CompetencyProgressRepository competencyProgressRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository, StandardizedCompetencyRepository standardizedCompetencyRepository, CourseCompetencyRepository courseCompetencyRepository, ExerciseService exerciseService, - LearningObjectImportService learningObjectImportService) { + LearningObjectImportService learningObjectImportService, CourseRepository courseRepository) { super(competencyProgressRepository, courseCompetencyRepository, competencyRelationRepository, competencyProgressService, exerciseService, lectureUnitService, - learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService); + learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService, courseRepository); this.competencyRepository = competencyRepository; } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java index 01eb37cf8271..ea31bff10fad 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/CourseCompetencyService.java @@ -27,6 +27,7 @@ import de.tum.cit.aet.artemis.atlas.dto.CompetencyImportOptionsDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyRelationDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; +import de.tum.cit.aet.artemis.atlas.dto.UpdateCourseCompetencyRelationDTO; import de.tum.cit.aet.artemis.atlas.repository.CompetencyProgressRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository; import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; @@ -39,6 +40,7 @@ import de.tum.cit.aet.artemis.core.dto.pageablesearch.CompetencyPageableSearchDTO; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; +import de.tum.cit.aet.artemis.core.repository.CourseRepository; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.core.util.PageUtil; import de.tum.cit.aet.artemis.exercise.domain.Exercise; @@ -77,11 +79,13 @@ public class CourseCompetencyService { private final LearningObjectImportService learningObjectImportService; + private final CourseRepository courseRepository; + public CourseCompetencyService(CompetencyProgressRepository competencyProgressRepository, CourseCompetencyRepository courseCompetencyRepository, CompetencyRelationRepository competencyRelationRepository, CompetencyProgressService competencyProgressService, ExerciseService exerciseService, LectureUnitService lectureUnitService, LearningPathService learningPathService, AuthorizationCheckService authCheckService, StandardizedCompetencyRepository standardizedCompetencyRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository, - LearningObjectImportService learningObjectImportService) { + LearningObjectImportService learningObjectImportService, CourseRepository courseRepository) { this.competencyProgressRepository = competencyProgressRepository; this.courseCompetencyRepository = courseCompetencyRepository; this.competencyRelationRepository = competencyRelationRepository; @@ -93,6 +97,7 @@ public CourseCompetencyService(CompetencyProgressRepository competencyProgressRe this.standardizedCompetencyRepository = standardizedCompetencyRepository; this.lectureUnitCompletionRepository = lectureUnitCompletionRepository; this.learningObjectImportService = learningObjectImportService; + this.courseRepository = courseRepository; } /** @@ -123,6 +128,28 @@ public List findCourseCompetenciesWithProgressForUserByCourseI return findProgressForCompetenciesAndUser(competencies, userId); } + /** + * Updates the type of a course competency relation. + * + * @param courseId The id of the course for which to fetch the competencies + * @param courseCompetencyRelationId The id of the course competency relation to update + * @param updateCourseCompetencyRelationDTO The DTO containing the new relation type + * + */ + public void updateCourseCompetencyRelation(long courseId, long courseCompetencyRelationId, UpdateCourseCompetencyRelationDTO updateCourseCompetencyRelationDTO) { + var relation = competencyRelationRepository.findByIdElseThrow(courseCompetencyRelationId); + var course = courseRepository.findByIdElseThrow(courseId); + var headCompetency = relation.getHeadCompetency(); + var tailCompetency = relation.getTailCompetency(); + + if (!course.getId().equals(headCompetency.getCourse().getId()) || !course.getId().equals(tailCompetency.getCourse().getId())) { + throw new BadRequestAlertException("The relation does not belong to the course", ENTITY_NAME, "relationWrongCourse"); + } + + relation.setType(updateCourseCompetencyRelationDTO.newRelationType()); + competencyRelationRepository.save(relation); + } + /** * Search for all course competencies fitting a {@link CompetencyPageableSearchDTO search query}. The result is paged. * diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java index 3fc520a21378..4bf07e6e42ca 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/service/competency/PrerequisiteService.java @@ -23,6 +23,7 @@ import de.tum.cit.aet.artemis.atlas.service.LearningObjectImportService; import de.tum.cit.aet.artemis.atlas.service.learningpath.LearningPathService; import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.repository.CourseRepository; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.exercise.service.ExerciseService; import de.tum.cit.aet.artemis.lecture.repository.LectureUnitCompletionRepository; @@ -41,9 +42,9 @@ public PrerequisiteService(PrerequisiteRepository prerequisiteRepository, Author LearningPathService learningPathService, CompetencyProgressService competencyProgressService, LectureUnitService lectureUnitService, CompetencyProgressRepository competencyProgressRepository, LectureUnitCompletionRepository lectureUnitCompletionRepository, StandardizedCompetencyRepository standardizedCompetencyRepository, CourseCompetencyRepository courseCompetencyRepository, ExerciseService exerciseService, - LearningObjectImportService learningObjectImportService) { + LearningObjectImportService learningObjectImportService, CourseRepository courseRepository) { super(competencyProgressRepository, courseCompetencyRepository, competencyRelationRepository, competencyProgressService, exerciseService, lectureUnitService, - learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService); + learningPathService, authCheckService, standardizedCompetencyRepository, lectureUnitCompletionRepository, learningObjectImportService, courseRepository); this.prerequisiteRepository = prerequisiteRepository; } diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java index 449a92a1d171..8e93c6c73090 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/CourseCompetencyResource.java @@ -10,6 +10,7 @@ import java.util.Set; import java.util.stream.Collectors; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import org.slf4j.Logger; @@ -18,6 +19,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -32,6 +34,7 @@ import de.tum.cit.aet.artemis.atlas.dto.CompetencyJolPairDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyRelationDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; +import de.tum.cit.aet.artemis.atlas.dto.UpdateCourseCompetencyRelationDTO; import de.tum.cit.aet.artemis.atlas.repository.CompetencyProgressRepository; import de.tum.cit.aet.artemis.atlas.repository.CompetencyRelationRepository; import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository; @@ -350,6 +353,23 @@ public ResponseEntity generateCompetenciesFromCourseDescription(@PathVaria return ResponseEntity.accepted().build(); } + /** + * PATCH courses/:courseId/course-competencies/relations/:competencyRelationId update a relation type of an existing relation + * + * @param courseId the id of the course to which the competencies belong + * @param competencyRelationId the id of the competency relation to update + * @param updateCourseCompetencyRelationDTO the new relation type + * @return the ResponseEntity with status 200 (OK) + */ + @PatchMapping("courses/{courseId}/course-competencies/relations/{competencyRelationId}") + @EnforceAtLeastInstructorInCourse + public ResponseEntity updateCompetencyRelation(@PathVariable long courseId, @PathVariable long competencyRelationId, + @RequestBody @Valid UpdateCourseCompetencyRelationDTO updateCourseCompetencyRelationDTO) { + log.info("REST request to update a competency relation: {}", competencyRelationId); + courseCompetencyService.updateCourseCompetencyRelation(courseId, competencyRelationId, updateCourseCompetencyRelationDTO); + return ResponseEntity.noContent().build(); + } + /** * PUT courses/:courseId/course-competencies/:competencyId/jol/:jolValue : Sets the judgement of learning for a competency * diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-management-table.component.ts b/src/main/webapp/app/course/competencies/competency-management/competency-management-table.component.ts index e60ec966042a..0ee37dd08169 100644 --- a/src/main/webapp/app/course/competencies/competency-management/competency-management-table.component.ts +++ b/src/main/webapp/app/course/competencies/competency-management/competency-management-table.component.ts @@ -1,15 +1,7 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, inject } from '@angular/core'; import { CompetencyService } from 'app/course/competencies/competency.service'; import { AlertService } from 'app/core/util/alert.service'; -import { - CompetencyRelation, - CompetencyRelationDTO, - CompetencyWithTailRelationDTO, - CourseCompetency, - CourseCompetencyType, - dtoToCompetencyRelation, - getIcon, -} from 'app/entities/competency.model'; +import { CompetencyWithTailRelationDTO, CourseCompetency, CourseCompetencyType, getIcon } from 'app/entities/competency.model'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { filter, map } from 'rxjs/operators'; import { onError } from 'app/shared/util/global.utils'; @@ -30,7 +22,6 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; export class CompetencyManagementTableComponent implements OnInit, OnDestroy { @Input() courseId: number; @Input() courseCompetencies: CourseCompetency[]; - @Input() relations: CompetencyRelation[]; @Input() competencyType: CourseCompetencyType; @Input() standardizedCompetenciesEnabled: boolean; @@ -103,14 +94,7 @@ export class CompetencyManagementTableComponent implements OnInit, OnDestroy { */ updateDataAfterImportAll(res: Array) { const importedCompetencies = res.map((dto) => dto.competency).filter((element): element is CourseCompetency => !!element); - - const importedRelations = res - .map((dto) => dto.tailRelations) - .flat() - .filter((element): element is CompetencyRelationDTO => !!element) - .map((dto) => dtoToCompetencyRelation(dto)); this.courseCompetencies.push(...importedCompetencies); - this.relations.push(...importedRelations); } /** diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.html b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.html index e541845c4ede..a70934974f15 100644 --- a/src/main/webapp/app/course/competencies/competency-management/competency-management.component.html +++ b/src/main/webapp/app/course/competencies/competency-management/competency-management.component.html @@ -14,6 +14,10 @@

} + - -
-
- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- -
-
-
- @if (relationError) { - - } - -
-
- - - - - - - - - - - {{ node.label }} - - - - - - - - - {{ ('artemisApp.competency.relation.type.' + link.label | artemisTranslate).toUpperCase() }} - - - - - -
-
-
- - - diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.scss b/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.scss deleted file mode 100644 index e639f3e1bbfe..000000000000 --- a/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.scss +++ /dev/null @@ -1,28 +0,0 @@ -.accordion-body { - overflow: hidden; - max-height: 60vh; -} - -.node { - text { - fill: var(--body-color); - } - - rect { - fill: var(--primary); - } -} - -.edge { - stroke: var(--body-color) !important; - marker-end: url(#arrow); -} - -#arrow { - stroke: var(--body-color); - fill: var(--body-color); -} - -.text-path { - fill: var(--body-color); -} diff --git a/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.ts b/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.ts deleted file mode 100644 index 2fc93e4a8e63..000000000000 --- a/src/main/webapp/app/course/competencies/competency-management/competency-relation-graph.component.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { Component, EventEmitter, Output, computed, input } from '@angular/core'; -import { faArrowsToEye } from '@fortawesome/free-solid-svg-icons'; -import { Edge, NgxGraphZoomOptions, Node } from '@swimlane/ngx-graph'; -import { CompetencyRelation, CompetencyRelationError, CompetencyRelationType, CourseCompetency } from 'app/entities/competency.model'; -import { Subject } from 'rxjs'; - -@Component({ - selector: 'jhi-competency-relation-graph', - templateUrl: './competency-relation-graph.component.html', - styleUrls: ['./competency-relation-graph.component.scss'], -}) -export class CompetencyRelationGraphComponent { - competencies = input([]); - relations = input([]); - - @Output() onRemoveRelation = new EventEmitter(); - @Output() onCreateRelation = new EventEmitter(); - - nodes = computed(() => { - this.update$.next(true); - return this.competencies().map((competency): Node => { - return { - id: `${competency.id}`, - label: competency.title, - }; - }); - }); - - edges = computed(() => { - this.update$.next(true); - return this.relations().map( - (relation): Edge => ({ - id: `edge${relation.id}`, - source: `${relation.tailCompetency?.id}`, - target: `${relation.headCompetency?.id}`, - label: relation.type, - data: { - id: relation.id, - }, - }), - ); - }); - - tailCompetencyId?: number; - headCompetencyId?: number; - relationType?: CompetencyRelationType; - relationError?: CompetencyRelationError = undefined; - update$: Subject = new Subject(); - center$: Subject = new Subject(); - zoomToFit$: Subject = new Subject(); - - // icons - protected readonly faArrowsToEye = faArrowsToEye; - - // constants - protected readonly competencyRelationType = CompetencyRelationType; - protected readonly errorMessage: Record = { - CIRCULAR: 'artemisApp.competency.relation.createsCircularRelation', - EXISTING: 'artemisApp.competency.relation.relationAlreadyExists', - SELF: 'artemisApp.competency.relation.selfRelation', - }; - - /** - * creates a relation with the currently entered data if it would not cause an error - */ - createRelation() { - this.validate(); - if (this.relationError) { - return; - } - const relation: CompetencyRelation = { - tailCompetency: { id: this.tailCompetencyId }, - headCompetency: { id: this.headCompetencyId }, - type: this.relationType, - }; - this.onCreateRelation.emit(relation); - } - - /** - * removes the relation - * @param edge the edge symbolizing the relation - */ - removeRelation(edge: Edge) { - this.onRemoveRelation.emit(edge.data.id); - } - - centerView() { - this.zoomToFit$.next({ autoCenter: true }); - this.center$.next(true); - } - - /** - * Validates if the currently entered data would cause an error and sets relationError accordingly - */ - validate(): void { - if (!this.tailCompetencyId || !this.headCompetencyId || !this.relationType) { - this.relationError = undefined; - return; - } - if (this.headCompetencyId === this.tailCompetencyId) { - this.relationError = CompetencyRelationError.SELF; - return; - } - if (this.doesRelationAlreadyExist()) { - this.relationError = CompetencyRelationError.EXISTING; - return; - } - if (this.containsCircularRelation()) { - this.relationError = CompetencyRelationError.CIRCULAR; - return; - } - this.relationError = undefined; - } - - /** - * checks if the currently entered data is equal to an existing relation - * @private - */ - private doesRelationAlreadyExist(): boolean { - return !!this.edges().find((edge) => edge.source === this.tailCompetencyId?.toString() && edge.target === this.headCompetencyId?.toString()); - } - - /** - * Checks if the currently entered data would create a circular relation - * - * @private - */ - private containsCircularRelation(): boolean { - if (!this.tailCompetencyId || !this.headCompetencyId || !this.relationType) { - return false; - } - return this.doesCreateCircularRelation(this.nodes(), this.edges(), { - source: this.tailCompetencyId! + '', - target: this.headCompetencyId! + '', - label: this.relationType!, - } as Edge); - } - - /** - * Checks if adding an edge would create a circular relation - * @param {Node[]} nodes an array of all existing nodes of a graph - * @param {Edge[]} edges an array of all existing edges of a graph - * @param {Edge} edgeToAdd the edge that you try to add to the graph - * - * @returns {boolean} whether or not adding the provided edge would result in a circle in the graph - */ - private doesCreateCircularRelation(nodes: Node[], edges: Edge[], edgeToAdd: Edge): boolean { - const edgesWithNewEdge = JSON.parse(JSON.stringify(edges)); - edgesWithNewEdge.push(edgeToAdd); - const graph = new Graph(); - for (const node of nodes) { - graph.addVertex(new Vertex(node.id)); - } - for (const edge of edgesWithNewEdge) { - const headVertex = graph.vertices.find((vertex: Vertex) => vertex.getLabel() === edge.target); - const tailVertex = graph.vertices.find((vertex: Vertex) => vertex.getLabel() === edge.source); - if (headVertex === undefined || tailVertex === undefined) { - throw new TypeError('Every edge needs a source or a target.'); - } - // only extends and assumes relations are considered when checking for circles because only they don't make sense - // MATCHES relations are considered in the next step by merging the edges and combining the adjacencyLists - switch (edge.label) { - case 'EXTENDS': - case 'ASSUMES': { - graph.addEdge(tailVertex, headVertex); - break; - } - } - } - // combine vertices that are connected through MATCHES - for (const edge of edgesWithNewEdge) { - if (edge.label === 'MATCHES') { - const headVertex = graph.vertices.find((vertex: Vertex) => vertex.getLabel() === edge.target); - const tailVertex = graph.vertices.find((vertex: Vertex) => vertex.getLabel() === edge.source); - if (headVertex === undefined || tailVertex === undefined) { - throw new TypeError('Every edge needs a source or a target.'); - } - if (headVertex.getAdjacencyList().includes(tailVertex) || tailVertex.getAdjacencyList().includes(headVertex)) { - return true; - } - // create a merged vertex - const mergedVertex = new Vertex(tailVertex.getLabel() + ', ' + headVertex.getLabel()); - // add all neighbours to merged vertex - mergedVertex.getAdjacencyList().push(...headVertex.getAdjacencyList()); - mergedVertex.getAdjacencyList().push(...tailVertex.getAdjacencyList()); - // update every vertex that initially had one of the two merged vertices as neighbours to now reference the merged vertex - for (const vertex of graph.vertices) { - for (const adjacentVertex of vertex.getAdjacencyList()) { - if (adjacentVertex.getLabel() === headVertex.getLabel() || adjacentVertex.getLabel() === tailVertex.getLabel()) { - const index = vertex.getAdjacencyList().indexOf(adjacentVertex, 0); - if (index > -1) { - vertex.getAdjacencyList().splice(index, 1); - } - vertex.getAdjacencyList().push(mergedVertex); - } - } - } - } - } - return graph.hasCycle(); - } - - /** - * Keeps order of elements as-is in the keyvalue pipe - */ - keepOrder = () => { - return 0; - }; -} - -/** - * A class that represents a vertex in a graph - * @class - * - * @constructor - * - * @property label a label to identify the vertex (we use the node id) - * @property beingVisited is the vertex the one that is currently being visited during the graph traversal - * @property visited has this vertex been visited before - * @property adjacencyList an array that contains all adjacent vertices - */ -class Vertex { - private readonly label: string; - private beingVisited: boolean; - private visited: boolean; - private readonly adjacencyList: Vertex[]; - - constructor(label: string) { - this.label = label; - this.adjacencyList = []; - } - - getLabel(): string { - return this.label; - } - - addNeighbor(adjacent: Vertex): void { - this.adjacencyList.push(adjacent); - } - - getAdjacencyList(): Vertex[] { - return this.adjacencyList; - } - - isBeingVisited(): boolean { - return this.beingVisited; - } - - setBeingVisited(beingVisited: boolean): void { - this.beingVisited = beingVisited; - } - - isVisited(): boolean { - return this.visited; - } - - setVisited(visited: boolean) { - this.visited = visited; - } -} - -/** - * A class that represents a graph - * @class - * - * @constructor - * - * @property vertices an array of all vertices in the graph (edges are represented by the adjacent vertices property of each vertex) - */ -class Graph { - vertices: Vertex[]; - - constructor() { - this.vertices = []; - } - - public addVertex(vertex: Vertex): void { - this.vertices.push(vertex); - } - - public addEdge(from: Vertex, to: Vertex): void { - from.addNeighbor(to); - } - - /** - * Checks if the graph contains a circle - * - * @returns {boolean} whether or not the graph contains a circle - */ - public hasCycle(): boolean { - // we have to check for every vertex if it is part of a cycle in case the graph is not connected - for (const vertex of this.vertices) { - if (!vertex.isVisited() && this.vertexHasCycle(vertex)) { - return true; - } - } - return false; - } - - /** - * Checks if a vertex is part of a circle - * - * @returns {boolean} whether or not the vertex is part of a circle - */ - private vertexHasCycle(sourceVertex: Vertex): boolean { - sourceVertex.setBeingVisited(true); - - for (const neighbor of sourceVertex.getAdjacencyList()) { - if (neighbor.isBeingVisited() || (!neighbor.isVisited() && this.vertexHasCycle(neighbor))) { - // backward edge exists - return true; - } - } - - sourceVertex.setBeingVisited(false); - sourceVertex.setVisited(true); - return false; - } -} diff --git a/src/main/webapp/app/course/competencies/competency.module.ts b/src/main/webapp/app/course/competencies/competency.module.ts index d7b4c6da30b5..6065b7e90ccb 100644 --- a/src/main/webapp/app/course/competencies/competency.module.ts +++ b/src/main/webapp/app/course/competencies/competency.module.ts @@ -3,7 +3,6 @@ import { RouterModule } from '@angular/router'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { CompetencyManagementComponent } from './competency-management/competency-management.component'; import { CompetencyCardComponent } from 'app/course/competencies/competency-card/competency-card.component'; import { CompetenciesPopoverComponent } from './competencies-popover/competencies-popover.component'; import { NgxGraphModule } from '@swimlane/ngx-graph'; @@ -15,7 +14,6 @@ import { CourseDescriptionFormComponent } from 'app/course/competencies/generate import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; import { IrisModule } from 'app/iris/iris.module'; import { TaxonomySelectComponent } from 'app/course/competencies/taxonomy-select/taxonomy-select.component'; -import { CompetencyRelationGraphComponent } from 'app/course/competencies/competency-management/competency-relation-graph.component'; import { CompetencyAccordionComponent } from 'app/course/competencies/competency-accordion/competency-accordion.component'; import { ArtemisCourseExerciseRowModule } from 'app/overview/course-exercises/course-exercise-row.module'; import { RatingModule } from 'app/exercises/shared/rating/rating.module'; @@ -49,13 +47,11 @@ import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown CompetencySearchComponent, CompetencyRecommendationDetailComponent, CourseDescriptionFormComponent, - CompetencyManagementComponent, CompetencyCardComponent, CompetencyAccordionComponent, CompetenciesPopoverComponent, ImportCompetenciesTableComponent, TaxonomySelectComponent, - CompetencyRelationGraphComponent, ], exports: [ CompetencyCardComponent, diff --git a/src/main/webapp/app/course/competencies/components/course-competencies-relation-graph/course-competencies-relation-graph.component.html b/src/main/webapp/app/course/competencies/components/course-competencies-relation-graph/course-competencies-relation-graph.component.html new file mode 100644 index 000000000000..d90c81fd3714 --- /dev/null +++ b/src/main/webapp/app/course/competencies/components/course-competencies-relation-graph/course-competencies-relation-graph.component.html @@ -0,0 +1,36 @@ +
+ + + + + + + + + + + + + + + + + + + {{ ('artemisApp.courseCompetency.relations.relationTypes.' + link.label | artemisTranslate).toUpperCase() }} + + + + + +
diff --git a/src/main/webapp/app/course/competencies/components/course-competencies-relation-graph/course-competencies-relation-graph.component.scss b/src/main/webapp/app/course/competencies/components/course-competencies-relation-graph/course-competencies-relation-graph.component.scss new file mode 100644 index 000000000000..add2bfbd3928 --- /dev/null +++ b/src/main/webapp/app/course/competencies/components/course-competencies-relation-graph/course-competencies-relation-graph.component.scss @@ -0,0 +1,14 @@ +.course-competencies-graph-container { + #arrow { + stroke: var(--body-color); + fill: var(--body-color); + } + + .selected { + stroke: var(--bs-primary); + } + + .text-path { + fill: var(--body-color); + } +} diff --git a/src/main/webapp/app/course/competencies/components/course-competencies-relation-graph/course-competencies-relation-graph.component.ts b/src/main/webapp/app/course/competencies/components/course-competencies-relation-graph/course-competencies-relation-graph.component.ts new file mode 100644 index 000000000000..448d2e4b1d77 --- /dev/null +++ b/src/main/webapp/app/course/competencies/components/course-competencies-relation-graph/course-competencies-relation-graph.component.ts @@ -0,0 +1,80 @@ +import { Component, computed, effect, input, model, output, signal } from '@angular/core'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { faFileImport } from '@fortawesome/free-solid-svg-icons'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { CompetencyRelationDTO, CourseCompetency } from 'app/entities/competency.model'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { Edge, NgxGraphModule, Node } from '@swimlane/ngx-graph'; +import { Subject } from 'rxjs'; +import { SizeUpdate } from 'app/course/learning-paths/components/competency-node/competency-node.component'; +import { CourseCompetencyRelationNodeComponent } from 'app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component'; + +@Component({ + selector: 'jhi-course-competencies-relation-graph', + standalone: true, + imports: [FontAwesomeModule, NgbAccordionModule, NgxGraphModule, ArtemisSharedModule, CourseCompetencyRelationNodeComponent], + templateUrl: './course-competencies-relation-graph.component.html', + styleUrl: './course-competencies-relation-graph.component.scss', +}) +export class CourseCompetenciesRelationGraphComponent { + protected readonly faFileImport = faFileImport; + + readonly courseCompetencies = input.required(); + readonly relations = input.required(); + + readonly selectedRelationId = model.required(); + + readonly onCourseCompetencySelection = output(); + + readonly update$ = new Subject(); + readonly center$ = new Subject(); + + readonly nodes = signal([]); + + readonly edges = computed(() => { + return this.relations().map((relation) => ({ + id: `edge-${relation.id}`, + source: `${relation.headCompetencyId}`, + target: `${relation.tailCompetencyId}`, + label: relation.relationType, + data: { + id: relation.id, + }, + })); + }); + + constructor() { + effect( + () => { + return this.nodes.set( + this.courseCompetencies().map( + (courseCompetency): Node => ({ + id: courseCompetency.id!.toString(), + label: courseCompetency.title, + data: { + id: courseCompetency.id, + type: courseCompetency.type, + }, + }), + ), + ); + }, + { allowSignalWrites: true }, + ); + } + + protected selectRelation(relationId: number): void { + this.selectedRelationId.set(relationId); + } + + protected setNodeDimension(sizeUpdate: SizeUpdate): void { + this.nodes.update((nodes) => + nodes.map((node) => { + if (node.id === sizeUpdate.id) { + node.dimension = sizeUpdate.dimension; + } + return node; + }), + ); + } +} diff --git a/src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.html b/src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.html new file mode 100644 index 000000000000..5e14b7fb7680 --- /dev/null +++ b/src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.html @@ -0,0 +1,33 @@ +
+
+
+ + +
+
+
+
+ @if (isLoading()) { +
+
+ +
+
+ } @else { +
+ +
+ + } +
+
diff --git a/src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.scss b/src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.scss new file mode 100644 index 000000000000..bc1c8d310a04 --- /dev/null +++ b/src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.scss @@ -0,0 +1,5 @@ +.course-competencies-graph-modal { + height: 90vh; + max-height: 700px; + overflow: hidden; +} diff --git a/src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.ts b/src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.ts new file mode 100644 index 000000000000..df0547c57a44 --- /dev/null +++ b/src/main/webapp/app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component.ts @@ -0,0 +1,57 @@ +import { Component, effect, inject, input, signal, viewChild } from '@angular/core'; +import { CourseCompetencyApiService } from 'app/course/competencies/services/course-competency-api.service'; +import { CompetencyRelationDTO, CourseCompetency } from 'app/entities/competency.model'; +import { AlertService } from 'app/core/util/alert.service'; +import { onError } from 'app/shared/util/global.utils'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { CompetencyGraphComponent } from 'app/course/learning-paths/components/competency-graph/competency-graph.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { CourseCompetencyRelationFormComponent } from 'app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component'; +import { CourseCompetenciesRelationGraphComponent } from '../course-competencies-relation-graph/course-competencies-relation-graph.component'; + +@Component({ + selector: 'jhi-course-competencies-relation-modal', + standalone: true, + imports: [ArtemisSharedCommonModule, CompetencyGraphComponent, CourseCompetenciesRelationGraphComponent, CourseCompetencyRelationFormComponent], + templateUrl: './course-competencies-relation-modal.component.html', + styleUrl: './course-competencies-relation-modal.component.scss', +}) +export class CourseCompetenciesRelationModalComponent { + private readonly courseCompetencyApiService = inject(CourseCompetencyApiService); + private readonly alertService = inject(AlertService); + private readonly activeModal = inject(NgbActiveModal); + + private readonly courseCompetencyRelationFormComponent = viewChild.required(CourseCompetencyRelationFormComponent); + + readonly courseId = input.required(); + readonly courseCompetencies = input.required(); + + readonly selectedRelationId = signal(undefined); + + readonly isLoading = signal(false); + readonly relations = signal([]); + + constructor() { + effect(() => this.loadRelations(this.courseId()), { allowSignalWrites: true }); + } + + private async loadRelations(courseId: number): Promise { + try { + this.isLoading.set(true); + const relations = await this.courseCompetencyApiService.getCourseCompetencyRelationsByCourseId(courseId); + this.relations.set(relations); + } catch (error) { + onError(this.alertService, error); + } finally { + this.isLoading.set(false); + } + } + + protected selectCourseCompetency(courseCompetencyId: number) { + this.courseCompetencyRelationFormComponent().selectCourseCompetency(courseCompetencyId); + } + + protected closeModal(): void { + this.activeModal.close(); + } +} diff --git a/src/main/webapp/app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component.html b/src/main/webapp/app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component.html new file mode 100644 index 000000000000..5d8be2495c7c --- /dev/null +++ b/src/main/webapp/app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component.html @@ -0,0 +1,70 @@ +
+
+ + +
+
+ + +
+
+ + +
+
+ @if (exactRelationAlreadyExists()) { + + } @else if (relationAlreadyExists()) { + + } @else { + + } +
+ @if (showCircularDependencyError()) { + + } +
diff --git a/src/main/webapp/app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component.scss b/src/main/webapp/app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component.scss new file mode 100644 index 000000000000..84ed3ff63b6a --- /dev/null +++ b/src/main/webapp/app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component.scss @@ -0,0 +1,4 @@ +.course-competency-relation-form-container { + background-color: var(--bs-body-bg); + border-radius: var(--bs-border-radius-lg); +} diff --git a/src/main/webapp/app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component.ts b/src/main/webapp/app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component.ts new file mode 100644 index 000000000000..7f173f4968a3 --- /dev/null +++ b/src/main/webapp/app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component.ts @@ -0,0 +1,306 @@ +import { Component, computed, effect, inject, input, model, signal } from '@angular/core'; +import { CompetencyRelationDTO, CompetencyRelationType, CourseCompetency, UpdateCourseCompetencyRelationDTO } from 'app/entities/competency.model'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { CourseCompetencyApiService } from 'app/course/competencies/services/course-competency-api.service'; +import { AlertService } from 'app/core/util/alert.service'; +import { faSpinner } from '@fortawesome/free-solid-svg-icons'; + +@Component({ + selector: 'jhi-course-competency-relation-form', + standalone: true, + imports: [ArtemisSharedCommonModule], + templateUrl: './course-competency-relation-form.component.html', + styleUrl: './course-competency-relation-form.component.scss', +}) +export class CourseCompetencyRelationFormComponent { + protected readonly faSpinner = faSpinner; + + protected readonly competencyRelationType = CompetencyRelationType; + + private readonly courseCompetencyApiService = inject(CourseCompetencyApiService); + private readonly alertService = inject(AlertService); + + readonly courseId = input.required(); + readonly courseCompetencies = input.required(); + readonly relations = model.required(); + readonly selectedRelationId = model.required(); + + readonly headCompetencyId = signal(undefined); + readonly tailCompetencyId = signal(undefined); + readonly relationType = model(undefined); + + readonly isLoading = signal(false); + + readonly relationAlreadyExists = computed(() => this.getRelation(this.headCompetencyId(), this.tailCompetencyId()) !== undefined); + readonly exactRelationAlreadyExists = computed(() => this.getExactRelation(this.headCompetencyId(), this.tailCompetencyId(), this.relationType()) !== undefined); + + private readonly selectableTailCourseCompetencyIds = computed(() => { + if (this.headCompetencyId() && this.relationType()) { + return this.getSelectableTailCompetencyIds(this.headCompetencyId()!, this.relationType()!); + } + return this.courseCompetencies().map(({ id }) => id!); + }); + + readonly showCircularDependencyError = computed(() => this.tailCompetencyId() && !this.selectableTailCourseCompetencyIds().includes(this.tailCompetencyId()!)); + + constructor() { + effect(() => this.selectRelation(this.selectedRelationId()), { allowSignalWrites: true }); + } + + protected isCourseCompetencySelectable(courseCompetencyId: number): boolean { + return this.selectableTailCourseCompetencyIds().includes(courseCompetencyId); + } + + private selectRelation(relationId?: number): void { + const relation = this.relations().find(({ id }) => id === relationId); + if (relation) { + this.headCompetencyId.set(relation?.headCompetencyId); + this.tailCompetencyId.set(relation?.tailCompetencyId); + this.relationType.set(relation?.relationType); + } + } + + public selectCourseCompetency(courseCompetencyId: number): void { + if (!this.headCompetencyId()) { + this.selectHeadCourseCompetency(courseCompetencyId); + } else if (!this.tailCompetencyId()) { + this.selectTailCourseCompetency(courseCompetencyId); + } else { + this.selectHeadCourseCompetency(courseCompetencyId); + } + } + + protected selectHeadCourseCompetency(headId: number) { + this.headCompetencyId.set(headId); + this.tailCompetencyId.set(undefined); + this.selectedRelationId.set(undefined); + } + + protected selectTailCourseCompetency(tailId: number) { + this.tailCompetencyId.set(tailId); + const existingRelation = this.getRelation(this.headCompetencyId(), this.tailCompetencyId()); + if (existingRelation) { + this.selectedRelationId.set(existingRelation.id); + } else { + this.selectedRelationId.set(undefined); + } + } + + protected async createRelation(): Promise { + try { + this.isLoading.set(true); + const courseCompetencyRelation = await this.courseCompetencyApiService.createCourseCompetencyRelation(this.courseId(), { + headCompetencyId: this.headCompetencyId()!, + tailCompetencyId: Number(this.tailCompetencyId()!), + relationType: this.relationType()!, + }); + this.relations.update((relations) => [...relations, courseCompetencyRelation]); + this.selectedRelationId.set(courseCompetencyRelation.id!); + } catch (error) { + this.alertService.error(error.message); + } finally { + this.isLoading.set(false); + } + } + + protected getExactRelation(headCompetencyId?: number, tailCompetencyId?: number, relationType?: CompetencyRelationType): CompetencyRelationDTO | undefined { + return this.relations().find( + (relation) => relation.headCompetencyId === headCompetencyId && relation.tailCompetencyId === tailCompetencyId && relation.relationType === relationType, + ); + } + + protected getRelation(headCompetencyId?: number, tailCompetencyId?: number): CompetencyRelationDTO | undefined { + return this.relations().find((relation) => relation.headCompetencyId === headCompetencyId && relation.tailCompetencyId === tailCompetencyId); + } + + protected async updateRelation(): Promise { + try { + this.isLoading.set(true); + const newRelationType = this.relationType()!; + await this.courseCompetencyApiService.updateCourseCompetencyRelation(this.courseId(), this.selectedRelationId()!, { + newRelationType: newRelationType, + }); + this.relations.update((relations) => + relations.map((relation) => { + if (relation.id === this.selectedRelationId()) { + return { ...relation, relationType: newRelationType }; + } + return relation; + }), + ); + } catch (error) { + this.alertService.error(error.message); + } finally { + this.isLoading.set(false); + } + } + + protected async deleteRelation(): Promise { + try { + this.isLoading.set(true); + const deletedRelation = this.relations().find( + ({ headCompetencyId, tailCompetencyId, relationType }) => + headCompetencyId == this.headCompetencyId() && tailCompetencyId == this.tailCompetencyId() && relationType === this.relationType(), + ); + await this.courseCompetencyApiService.deleteCourseCompetencyRelation(this.courseId(), deletedRelation!.id!); + this.relations.update((relations) => relations.filter(({ id }) => id !== deletedRelation!.id)); + this.selectedRelationId.set(undefined); + } catch (error) { + this.alertService.error(error.message); + } finally { + this.isLoading.set(false); + } + } + + /** + * Function to get the selectable tail competency ids for the given head + * competency and relation type without creating a cyclic dependency + * + * @param headCompetencyId The selected head competency id + * @param relationType The selected relation type + * @private + * + * @returns The selectable tail competency ids + */ + private getSelectableTailCompetencyIds(headCompetencyId: number, relationType: CompetencyRelationType): number[] { + return this.courseCompetencies() + .map(({ id }) => id!) + .filter((id) => id !== headCompetencyId) // Exclude the head itself + .filter((id) => { + let relations = this.relations(); + const existingRelation = this.getRelation(headCompetencyId, id); + if (existingRelation) { + relations = relations.filter((relation) => relation.id !== existingRelation.id); + } + const potentialRelation: CompetencyRelationDTO = { + headCompetencyId: headCompetencyId, + tailCompetencyId: id, + relationType: relationType, + }; + return !this.detectCycleInRelations(relations.concat(potentialRelation), this.courseCompetencies().length); + }); + } + + /** + * Function to detect cycles in the competency relations + * @param relations The list of competency relations + * @param numOfCompetencies The total number of competencies + * @private + * + * @returns True if a cycle is detected, false otherwise + */ + private detectCycleInRelations(relations: CompetencyRelationDTO[], numOfCompetencies: number): boolean { + // Create a map to store the competency IDs and map them to incremental indices + const idToIndexMap = new Map(); + let currentIndex = 0; + + // map the competency IDs to incremental indices + relations.forEach((relation) => { + const tail = relation.tailCompetencyId!; + const head = relation.headCompetencyId!; + + if (!idToIndexMap.has(tail)) { + idToIndexMap.set(tail, currentIndex++); + } + if (!idToIndexMap.has(head)) { + idToIndexMap.set(head, currentIndex++); + } + }); + + const unionFind = new UnionFind(numOfCompetencies); + + // Apply Union-Find based on the MATCHES relations + relations.forEach((relation) => { + if (relation.relationType === CompetencyRelationType.MATCHES) { + const tailIndex = idToIndexMap.get(relation.tailCompetencyId!); + const headIndex = idToIndexMap.get(relation.headCompetencyId!); + + if (tailIndex !== undefined && headIndex !== undefined) { + // Perform union operation to group matching course competencies into sets + unionFind.union(tailIndex, headIndex); + } + } + }); + + // Build the reduced graph for EXTENDS and ASSUMES relations + const reducedGraph: number[][] = Array.from({ length: numOfCompetencies }, () => []); + + relations.forEach((relation) => { + const tail = unionFind.find(idToIndexMap.get(relation.tailCompetencyId!)!); + const head = unionFind.find(idToIndexMap.get(relation.headCompetencyId!)!); + + if (relation.relationType === CompetencyRelationType.EXTENDS || relation.relationType === CompetencyRelationType.ASSUMES) { + reducedGraph[tail].push(head); + } + }); + + return this.hasCycle(reducedGraph, numOfCompetencies); + } + + private hasCycle(graph: number[][], noOfCourseCompetencies: number): boolean { + const visited: boolean[] = Array(noOfCourseCompetencies).fill(false); + const recursionStack: boolean[] = Array(noOfCourseCompetencies).fill(false); + + // Depth-first search to detect cycles + const depthFirstSearch = (v: number): boolean => { + visited[v] = true; + recursionStack[v] = true; + + for (const neighbor of graph[v] || []) { + if (!visited[neighbor]) { + if (depthFirstSearch(neighbor)) return true; + } else if (recursionStack[neighbor]) { + return true; + } + } + + recursionStack[v] = false; + return false; + }; + + for (let node = 0; node < noOfCourseCompetencies; node++) { + if (!visited[node]) { + if (depthFirstSearch(node)) { + return true; + } + } + } + return false; + } +} + +// Union-Find (Disjoint Set) class (https://en.wikipedia.org/wiki/Disjoint-set_data_structure -> union by rank) +export class UnionFind { + parent: number[]; + rank: number[]; + + constructor(size: number) { + this.parent = Array.from({ length: size }, (_, index) => index); + this.rank = Array(size).fill(1); + } + + // Find the representative of the set that contains the `competencyId` + public find(competencyId: number): number { + if (this.parent[competencyId] !== competencyId) { + this.parent[competencyId] = this.find(this.parent[competencyId]); // Path compression + } + return this.parent[competencyId]; + } + + // Union the sets containing `tailCompetencyId` and `headCompetencyId` + public union(tailCompetencyId: number, headCompetencyId: number) { + const rootU = this.find(tailCompetencyId); + const rootV = this.find(headCompetencyId); + if (rootU !== rootV) { + // Union by rank + if (this.rank[rootU] > this.rank[rootV]) { + this.parent[rootV] = rootU; + } else if (this.rank[rootU] < this.rank[rootV]) { + this.parent[rootU] = rootV; + } else { + this.parent[rootV] = rootU; + this.rank[rootU] += 1; + } + } + } +} diff --git a/src/main/webapp/app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component.html b/src/main/webapp/app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component.html new file mode 100644 index 000000000000..c7c8821f577f --- /dev/null +++ b/src/main/webapp/app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component.html @@ -0,0 +1,17 @@ +
+
+ + + +
+ {{ courseCompetencyNode().label }} +
diff --git a/src/main/webapp/app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component.scss b/src/main/webapp/app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component.scss new file mode 100644 index 000000000000..baa3d06976c5 --- /dev/null +++ b/src/main/webapp/app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component.scss @@ -0,0 +1,20 @@ +.competency-node { + white-space: nowrap; + background-color: var(--bs-body-bg); + border-radius: calc(var(--bs-border-radius-lg) + 6px); + padding: 10px 12px; + + .progress-container { + color: var(--bs-white); + padding: 2px 8px; + border-radius: var(--bs-border-radius-lg); + } + + .competency-container { + background-color: var(--bs-green); + } + + .prerequisite-container { + background-color: var(--bs-yellow); + } +} diff --git a/src/main/webapp/app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component.ts b/src/main/webapp/app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component.ts new file mode 100644 index 000000000000..48708b6ed23c --- /dev/null +++ b/src/main/webapp/app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component.ts @@ -0,0 +1,38 @@ +import { AfterViewInit, Component, ElementRef, computed, inject, input, output } from '@angular/core'; +import { SizeUpdate } from 'app/course/learning-paths/components/competency-node/competency-node.component'; +import { Node } from '@swimlane/ngx-graph'; +import { CourseCompetencyType } from 'app/entities/competency.model'; +import { NgClass } from '@angular/common'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; + +@Component({ + selector: 'jhi-course-competency-relation-node', + standalone: true, + imports: [NgClass, TranslateDirective, NgbTooltipModule, ArtemisSharedModule], + templateUrl: './course-competency-relation-node.component.html', + styleUrl: './course-competency-relation-node.component.scss', +}) +export class CourseCompetencyRelationNodeComponent implements AfterViewInit { + protected readonly CourseCompetencyType = CourseCompetencyType; + // height of node element in pixels + private readonly nodeHeight = 45.59; + + private readonly element = inject(ElementRef); + + readonly courseCompetencyNode = input.required(); + readonly courseCompetencyType = computed(() => this.courseCompetencyNode().data.type!); + + readonly onSizeSet = output(); + + ngAfterViewInit(): void { + this.setDimensions(this.element); + } + + setDimensions(element: ElementRef): void { + const width: number = element.nativeElement.offsetWidth; + const height = this.nodeHeight; + this.onSizeSet.emit({ id: `${this.courseCompetencyNode().id}`, dimension: { height, width } }); + } +} diff --git a/src/main/webapp/app/course/competencies/services/course-competency-api.service.ts b/src/main/webapp/app/course/competencies/services/course-competency-api.service.ts index c7d69c3674c9..26dbb518ba68 100644 --- a/src/main/webapp/app/course/competencies/services/course-competency-api.service.ts +++ b/src/main/webapp/app/course/competencies/services/course-competency-api.service.ts @@ -1,6 +1,12 @@ import { Injectable } from '@angular/core'; import { BaseApiHttpService } from 'app/course/learning-paths/services/base-api-http.service'; -import { CompetencyRelationDTO, CompetencyWithTailRelationDTO, CourseCompetency, CourseCompetencyImportOptionsDTO } from 'app/entities/competency.model'; +import { + CompetencyRelationDTO, + CompetencyWithTailRelationDTO, + CourseCompetency, + CourseCompetencyImportOptionsDTO, + UpdateCourseCompetencyRelationDTO, +} from 'app/entities/competency.model'; @Injectable({ providedIn: 'root' }) export class CourseCompetencyApiService extends BaseApiHttpService { @@ -10,23 +16,31 @@ export class CourseCompetencyApiService extends BaseApiHttpService { return this.basePath.replace('$courseId', courseId.toString()); } - importAllByCourseId(courseId: number, courseCompetencyImportOptions: CourseCompetencyImportOptionsDTO): Promise { - return this.post(`${this.getPath(courseId)}/import-all`, courseCompetencyImportOptions); + async importAllByCourseId(courseId: number, courseCompetencyImportOptions: CourseCompetencyImportOptionsDTO): Promise { + return await this.post(`${this.getPath(courseId)}/import-all`, courseCompetencyImportOptions); } - createCourseCompetencyRelation(courseId: number, relation: CompetencyRelationDTO): Promise { - return this.post(`${this.getPath(courseId)}/relations`, relation); + async createCourseCompetencyRelation(courseId: number, relation: CompetencyRelationDTO): Promise { + return await this.post(`${this.getPath(courseId)}/relations`, relation); } - deleteCourseCompetencyRelation(courseId: number, relationId: number): Promise { - return this.delete(`${this.getPath(courseId)}/relations/${relationId}`); + async updateCourseCompetencyRelation(courseId: number, relationId: number, updateCourseCompetencyRelationDTO: UpdateCourseCompetencyRelationDTO): Promise { + return await this.patch(`${this.getPath(courseId)}/relations/${relationId}`, updateCourseCompetencyRelationDTO); } - getCourseCompetencyRelations(courseId: number): Promise { - return this.get(`${this.getPath(courseId)}/relations`); + async deleteCourseCompetencyRelation(courseId: number, relationId: number): Promise { + return await this.delete(`${this.getPath(courseId)}/relations/${relationId}`); } - getCourseCompetenciesByCourseId(courseId: number): Promise { - return this.get(`${this.getPath(courseId)}`); + async getCourseCompetencyRelationsByCourseId(courseId: number): Promise { + return await this.get(`${this.getPath(courseId)}/relations`); + } + + async getCourseCompetencyRelations(courseId: number): Promise { + return await this.get(`${this.getPath(courseId)}/relations`); + } + + async getCourseCompetenciesByCourseId(courseId: number): Promise { + return await this.get(`${this.getPath(courseId)}`); } } diff --git a/src/main/webapp/app/entities/competency.model.ts b/src/main/webapp/app/entities/competency.model.ts index 2a24f304e04c..b4bd6321b458 100644 --- a/src/main/webapp/app/entities/competency.model.ts +++ b/src/main/webapp/app/entities/competency.model.ts @@ -53,6 +53,10 @@ export abstract class BaseCompetency implements BaseEntity { taxonomy?: CompetencyTaxonomy; } +export interface UpdateCourseCompetencyRelationDTO { + newRelationType: CompetencyRelationType; +} + export abstract class CourseCompetency extends BaseCompetency { softDueDate?: dayjs.Dayjs; masteryThreshold?: number; diff --git a/src/main/webapp/i18n/de/competency.json b/src/main/webapp/i18n/de/competency.json index cb07c639df52..aaef36b9f34a 100644 --- a/src/main/webapp/i18n/de/competency.json +++ b/src/main/webapp/i18n/de/competency.json @@ -172,8 +172,9 @@ "softDueDate": "Empfohlen bis", "masteryThreshold": "Schwellwert zum Erreichen der Kompetenz", "manage": { - "helpButton": "Hilfe", - "importAllButton": "Alles eines Kurses importieren" + "importAllButton": "Alles eines Kurses importieren", + "editRelationsButton": "Beziehungen bearbeiten", + "helpButton": "Hilfe" }, "importSettings": { "importRelationsLabel": "Beziehungen importieren", @@ -242,6 +243,36 @@ "participationRate": "Teilnahmerate" } }, + "relations": { + "modalTitle": "Beziehungen zwischen Kurskompetenzen", + "relationTypes": { + "ASSUMES": "Setzt voraus", + "EXTENDS": "Erweitert", + "MATCHES": "Stimmt überein mit" + }, + "form": { + "headCourseCompetencyLabel": "Startkurskompetenz", + "headCourseCompetencyDefaultOption": "Wähle Startkurskompetenz", + "tailCourseCompetencyLabel": "Schlusskurskompetenz", + "tailCourseCompetencyDefaultOption": "Wähle Schlusskurskompetenz", + "relationTypeLabel": "Beziehungstyp", + "relationTypeDefaultOption": "Wähle Beziehungstyp", + "deleteRelationButtonLabel": "Beziehung löschen", + "updateRelationButtonLabel": "Beziehung aktualisieren", + "createRelationButtonLabel": "Beziehung erstellen", + "cyclicDependencyError": "Du kannst keine zyklischen Beziehungen zwischen Kurskompetenzen erstellen." + }, + "graph": { + "nodeTypes": { + "competency": "K", + "prerequisite": "V" + }, + "tooltips": { + "competency": "Dieser Knoten repräsentiert eine Kompetenz", + "prerequisite": "Dieser Knoten repräsentiert eine Voraussetzung" + } + } + }, "featureExplanation": { "title": "Einführung in Kurskompetenzen", "adaptiveLearning": { diff --git a/src/main/webapp/i18n/en/competency.json b/src/main/webapp/i18n/en/competency.json index c056c0d50d94..e4aedbe82c77 100644 --- a/src/main/webapp/i18n/en/competency.json +++ b/src/main/webapp/i18n/en/competency.json @@ -172,8 +172,9 @@ "softDueDate": "Recommended until", "masteryThreshold": "Mastery threshold", "manage": { - "helpButton": "Help", - "importAllButton": "Import all of a course" + "importAllButton": "Import all of a course", + "editRelationsButton": "Edit relations", + "helpButton": "Help" }, "importSettings": { "importRelationsLabel": "Import relations", @@ -242,6 +243,36 @@ "EVALUATE": "appraise, argue, choose, defend, judge, select, support, value, critique", "CREATE": "design, formulate, hypothesize, invent, plan, propose, write, assemble, construct, develop" }, + "relations": { + "modalTitle": "Course competency relations", + "relationTypes": { + "ASSUMES": "Assumes", + "EXTENDS": "Extends", + "MATCHES": "Matches" + }, + "form": { + "headCourseCompetencyLabel": "Head course competency", + "headCourseCompetencyDefaultOption": "Select head course competency", + "tailCourseCompetencyLabel": "Tail course competency", + "tailCourseCompetencyDefaultOption": "Select tail course competency", + "relationTypeLabel": "Relation Type", + "relationTypeDefaultOption": "Select relation type", + "deleteRelationButtonLabel": "Delete Relation", + "updateRelationButtonLabel": "Update Relation", + "createRelationButtonLabel": "Create Relation", + "cyclicDependencyError": "You cannot create a cyclic dependency between course competencies." + }, + "graph": { + "nodeTypes": { + "competency": "C", + "prerequisite": "P" + }, + "tooltips": { + "competency": "This node represents a Competency", + "prerequisite": "This node represents a Prerequisite" + } + } + }, "featureExplanation": { "title": "Introduction to course competencies", "adaptiveLearning": { diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java index df2c561bda7e..bccff7083c8c 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java @@ -27,6 +27,7 @@ import de.tum.cit.aet.artemis.atlas.dto.CompetencyImportResponseDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyRelationDTO; import de.tum.cit.aet.artemis.atlas.dto.CompetencyWithTailRelationDTO; +import de.tum.cit.aet.artemis.atlas.dto.UpdateCourseCompetencyRelationDTO; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.dto.CourseCompetencyProgressDTO; import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; @@ -467,6 +468,31 @@ void shouldReturnBadRequestForCircularRelations() throws Exception { request.post("/api/courses/" + course.getId() + "/course-competencies/relations", CompetencyRelationDTO.of(relation), HttpStatus.BAD_REQUEST); } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void shouldUpdateForInstructor() throws Exception { + var headCompetency = competencyUtilService.createCompetency(course); + var relationToCreate = new CompetencyRelation(); + relationToCreate.setTailCompetency(courseCompetency); + relationToCreate.setHeadCompetency(headCompetency); + relationToCreate.setType(RelationType.EXTENDS); + + request.postWithResponseBody("/api/courses/" + course.getId() + "/course-competencies/relations", CompetencyRelationDTO.of(relationToCreate), CompetencyRelation.class, + HttpStatus.OK); + + var relations = competencyRelationRepository.findAllWithHeadAndTailByCourseId(course.getId()); + assertThat(relations).hasSize(1); + var relation = relations.stream().findFirst().get(); + assertThat(relation.getType()).isEqualTo(RelationType.EXTENDS); + + request.patch("/api/courses/" + course.getId() + "/course-competencies/relations/" + relation.getId(), new UpdateCourseCompetencyRelationDTO(RelationType.MATCHES), + HttpStatus.NO_CONTENT); + + relations = competencyRelationRepository.findAllWithHeadAndTailByCourseId(course.getId()); + assertThat(relations).hasSize(1); + assertThat(relations.stream().findFirst().get().getType()).isEqualTo(RelationType.MATCHES); + } } @Test diff --git a/src/test/javascript/spec/component/competencies/competency-management/competency-management-table.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-management/competency-management-table.component.spec.ts index d38ba571bcdd..e15cefdbc52d 100644 --- a/src/test/javascript/spec/component/competencies/competency-management/competency-management-table.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-management/competency-management-table.component.spec.ts @@ -53,7 +53,6 @@ describe('CompetencyManagementTableComponent', () => { it('should handle import all data', () => { component.courseCompetencies = []; - component.relations = []; const responseBody: CompetencyWithTailRelationDTO[] = [ { competency: { id: 1 }, tailRelations: [] }, @@ -62,7 +61,6 @@ describe('CompetencyManagementTableComponent', () => { component.updateDataAfterImportAll(responseBody); expect(component.courseCompetencies).toHaveLength(2); - expect(component.relations).toHaveLength(1); }); it('should handle delete competency', () => { @@ -72,7 +70,6 @@ describe('CompetencyManagementTableComponent', () => { const competency2 = { id: 2, type: CourseCompetencyType.COMPETENCY }; component.service = competencyService; component.courseCompetencies = [competency1, competency2]; - component.relations = [{ id: 1, headCompetency: competency1, tailCompetency: competency2, type: CompetencyRelationType.ASSUMES }]; component.deleteCompetency(1); expect(deleteSpy).toHaveBeenCalledOnce(); diff --git a/src/test/javascript/spec/component/competencies/competency-management/competency-management.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-management/competency-management.component.spec.ts index b0305c56b76d..a21a670a38fa 100644 --- a/src/test/javascript/spec/component/competencies/competency-management/competency-management.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/competency-management/competency-management.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; -import { Competency, CompetencyRelation, CompetencyWithTailRelationDTO, CourseCompetencyProgress, CourseCompetencyType } from 'app/entities/competency.model'; +import { Competency, CompetencyWithTailRelationDTO, CourseCompetencyProgress, CourseCompetencyType } from 'app/entities/competency.model'; import { CompetencyManagementComponent } from 'app/course/competencies/competency-management/competency-management.component'; import { ActivatedRoute, provideRouter } from '@angular/router'; import { DeleteButtonDirective } from 'app/shared/delete-dialog/delete-button.directive'; @@ -24,7 +24,6 @@ import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.serv import { ProfileInfo } from 'app/shared/layouts/profiles/profile-info.model'; import { IrisCourseSettings } from 'app/entities/iris/settings/iris-settings.model'; import { PROFILE_IRIS } from 'app/app.constants'; -import { CompetencyRelationGraphStubComponent } from './competency-relation-graph-stub.component'; import { Prerequisite } from 'app/entities/prerequisite.model'; import { CompetencyManagementTableComponent } from 'app/course/competencies/competency-management/competency-management-table.component'; import { CourseCompetencyApiService } from 'app/course/competencies/services/course-competency-api.service'; @@ -42,7 +41,6 @@ describe('CompetencyManagementComponent', () => { let modalService: NgbModal; let getAllForCourseSpy: any; - let getCompetencyRelationsSpy: any; beforeEach(() => { TestBed.configureTestingModule({ @@ -50,7 +48,6 @@ describe('CompetencyManagementComponent', () => { declarations: [ CompetencyManagementComponent, MockHasAnyAuthorityDirective, - CompetencyRelationGraphStubComponent, MockComponent(DocumentationButtonComponent), MockComponent(ImportAllCompetenciesComponent), MockComponent(CompetencyManagementTableComponent), @@ -103,7 +100,6 @@ describe('CompetencyManagementComponent', () => { type: CourseCompetencyType.PREREQUISITE, } as Prerequisite, ]); - getCompetencyRelationsSpy = jest.spyOn(courseCompetencyApiService, 'getCourseCompetencyRelations').mockResolvedValue([{ id: 1 } as CompetencyRelation]); }); }); @@ -139,7 +135,6 @@ describe('CompetencyManagementComponent', () => { await component.loadData(); expect(getAllForCourseSpy).toHaveBeenCalledOnce(); - expect(getCompetencyRelationsSpy).toHaveBeenCalledOnce(); expect(component.competencies).toHaveLength(2); expect(component.prerequisites).toHaveLength(1); @@ -170,7 +165,6 @@ describe('CompetencyManagementComponent', () => { jest.spyOn(courseCompetencyApiService, 'importAllByCourseId').mockResolvedValue(importedCompetencies); await component.loadData(); const existingCompetencies = component.competencies.length; - const existingRelations = component.relations.length; const importButton = fixture.debugElement.query(By.css('#courseCompetencyImportAllButton')); importButton.nativeElement.click(); @@ -184,80 +178,5 @@ describe('CompetencyManagementComponent', () => { await fixture.whenStable(); expect(component.competencies).toHaveLength(existingCompetencies + 2); - expect(component.relations).toHaveLength(existingRelations + 1); - }); - - it('should handle create relation callback', async () => { - const relation: CompetencyRelation = { id: 1 }; - jest.spyOn(courseCompetencyApiService, 'createCourseCompetencyRelation').mockResolvedValue(relation); - - fixture.detectChanges(); - await fixture.whenStable(); - - const existingRelations = component.relations.length; - - const relationGraph: CompetencyRelationGraphStubComponent = fixture.debugElement.query(By.directive(CompetencyRelationGraphStubComponent)).componentInstance; - expect(relationGraph).toBeDefined(); - relationGraph.onCreateRelation.emit(relation); - - fixture.detectChanges(); - await fixture.whenStable(); - - expect(component.relations).toHaveLength(existingRelations + 1); - }); - - it('should handle remove relation callback', () => { - const modalRef = { - result: Promise.resolve(), - componentInstance: {}, - } as NgbModalRef; - jest.spyOn(modalService, 'open').mockReturnValue(modalRef); - - fixture.detectChanges(); - - const relationGraph: CompetencyRelationGraphStubComponent = fixture.debugElement.query(By.directive(CompetencyRelationGraphStubComponent)).componentInstance; - relationGraph.onRemoveRelation.emit(1); - fixture.detectChanges(); - - expect(modalService.open).toHaveBeenCalledOnce(); - }); - - it('should remove relation', async () => { - jest.spyOn(courseCompetencyApiService, 'deleteCourseCompetencyRelation').mockResolvedValue(); - fixture.detectChanges(); - component.relations = [{ id: 1, headCompetency: { id: 5 }, tailCompetency: { id: 3 } }]; - - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - - expect(component.relations).toHaveLength(1); - - await component['removeRelation'](1); - - expect(component.relations).toHaveLength(0); - }); - - it('should remove competency and its relation', () => { - component.competencies = [ - { id: 1, type: CourseCompetencyType.COMPETENCY }, - { id: 2, type: CourseCompetencyType.COMPETENCY }, - ]; - component.prerequisites = [{ id: 3, type: CourseCompetencyType.PREREQUISITE }]; - component.courseCompetencies = component.competencies.concat(component.prerequisites); - component.relations = [ - { id: 1, tailCompetency: component.competencies.first(), headCompetency: component.competencies.last() }, - { id: 2, tailCompetency: component.competencies.last(), headCompetency: component.prerequisites.first() }, - { id: 3, tailCompetency: component.prerequisites.first(), headCompetency: component.competencies.first() }, - ]; - - component.onRemoveCompetency(2); - - expect(component.relations).toHaveLength(1); - expect(component.relations.first()?.id).toBe(3); - expect(component.competencies).toHaveLength(1); - expect(component.competencies.first()?.id).toBe(1); - expect(component.prerequisites).toHaveLength(1); - expect(component.prerequisites.first()?.id).toBe(3); }); }); diff --git a/src/test/javascript/spec/component/competencies/competency-management/competency-relation-graph-stub.component.ts b/src/test/javascript/spec/component/competencies/competency-management/competency-relation-graph-stub.component.ts deleted file mode 100644 index af5d302a6e1f..000000000000 --- a/src/test/javascript/spec/component/competencies/competency-management/competency-relation-graph-stub.component.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Component, EventEmitter, Output, input } from '@angular/core'; -import { Competency, CompetencyRelation } from 'app/entities/competency.model'; - -@Component({ - selector: 'jhi-competency-relation-graph', - template: '', -}) -export class CompetencyRelationGraphStubComponent { - competencies = input([]); - relations = input([]); - - @Output() onRemoveRelation = new EventEmitter(); - @Output() onCreateRelation = new EventEmitter(); -} diff --git a/src/test/javascript/spec/component/competencies/competency-management/competency-relation-graph.component.spec.ts b/src/test/javascript/spec/component/competencies/competency-management/competency-relation-graph.component.spec.ts deleted file mode 100644 index c60016292749..000000000000 --- a/src/test/javascript/spec/component/competencies/competency-management/competency-relation-graph.component.spec.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { ArtemisTestModule } from '../../../test.module'; -import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockDirective, MockPipe } from 'ng-mocks'; -import { Component } from '@angular/core'; -import { Competency, CompetencyRelation, CompetencyRelationError, CompetencyRelationType } from 'app/entities/competency.model'; -import { CompetencyRelationGraphComponent } from 'app/course/competencies/competency-management/competency-relation-graph.component'; -import { NgbAccordionBody, NgbAccordionButton, NgbAccordionCollapse, NgbAccordionDirective, NgbAccordionHeader, NgbAccordionItem } from '@ng-bootstrap/ng-bootstrap'; -import { Edge, Node } from '@swimlane/ngx-graph'; - -// eslint-disable-next-line @angular-eslint/component-selector -@Component({ selector: 'ngx-graph', template: '' }) -class NgxGraphStubComponent {} - -describe('CompetencyRelationGraphComponent', () => { - let componentFixture: ComponentFixture; - let component: CompetencyRelationGraphComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ArtemisTestModule], - declarations: [ - CompetencyRelationGraphComponent, - MockPipe(ArtemisTranslatePipe), - MockDirective(NgbAccordionDirective), - MockDirective(NgbAccordionItem), - MockDirective(NgbAccordionHeader), - MockDirective(NgbAccordionButton), - MockDirective(NgbAccordionCollapse), - MockDirective(NgbAccordionBody), - NgxGraphStubComponent, - ], - providers: [], - }) - .compileComponents() - .then(() => { - componentFixture = TestBed.createComponent(CompetencyRelationGraphComponent); - component = componentFixture.componentInstance; - }); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('should initialize', () => { - componentFixture.detectChanges(); - expect(component).toBeDefined(); - }); - - it('should correctly update nodes and edges from input', () => { - componentFixture.componentRef.setInput('competencies', [createCompetency(1, 'Competency 1'), createCompetency(2, 'Competency 2')]); - componentFixture.componentRef.setInput('relations', [createRelation(1, 1, 2, CompetencyRelationType.EXTENDS)]); - componentFixture.detectChanges(); - - const expectedNodes: Node[] = [ - { id: '1', label: 'Competency 1' }, - { id: '2', label: 'Competency 2' }, - ]; - const expectedEdges: Edge[] = [{ id: 'edge1', source: '1', target: '2', label: CompetencyRelationType.EXTENDS, data: { id: 1 } }]; - - expect(component.nodes()).toEqual(expectedNodes); - expect(component.edges()).toEqual(expectedEdges); - }); - - it('should create competency relation', () => { - componentFixture.componentRef.setInput('competencies', [createCompetency(1, 'Competency 1'), createCompetency(2, 'Competency 2')]); - component.tailCompetencyId = 1; - component.headCompetencyId = 2; - component.relationType = CompetencyRelationType.ASSUMES; - const relation: CompetencyRelation = { - tailCompetency: { id: component.tailCompetencyId }, - headCompetency: { id: component.headCompetencyId }, - type: component.relationType, - }; - const onCreateRelationSpy = jest.spyOn(component.onCreateRelation, 'emit'); - - componentFixture.detectChanges(); - - component.createRelation(); - expect(onCreateRelationSpy).toHaveBeenCalledWith(relation); - }); - - it('should not competency relation on error', () => { - component.tailCompetencyId = 1; - component.headCompetencyId = 1; - component.relationType = CompetencyRelationType.ASSUMES; - const onCreateRelationSpy = jest.spyOn(component.onCreateRelation, 'emit'); - const validateSpy = jest.spyOn(component, 'validate'); - - component.createRelation(); - expect(onCreateRelationSpy).not.toHaveBeenCalled(); - expect(validateSpy).toHaveBeenCalledOnce(); - }); - - it('should remove competency relation', () => { - const onRemoveRelationSpy = jest.spyOn(component.onRemoveRelation, 'emit'); - - component.removeRelation({ source: '123', data: { id: 456 } } as Edge); - expect(onRemoveRelationSpy).toHaveBeenCalledWith(456); - }); - - it('should detect circles on relations', () => { - const competencies = [createCompetency(16, '16'), createCompetency(17, '17'), createCompetency(18, '18')]; - const relations = [createRelation(1, 16, 17, CompetencyRelationType.EXTENDS), createRelation(1, 17, 18, CompetencyRelationType.MATCHES)]; - componentFixture.componentRef.setInput('competencies', competencies); - componentFixture.componentRef.setInput('relations', relations); - componentFixture.detectChanges(); - - component.tailCompetencyId = 18; - component.headCompetencyId = 16; - component.relationType = CompetencyRelationType.ASSUMES; - - component.validate(); - - expect(component.relationError).toBe(CompetencyRelationError.CIRCULAR); - }); - - it('should not detect circles on arbitrary relations', () => { - const competencies = [createCompetency(16, '16'), createCompetency(17, '17')]; - const relations: CompetencyRelation[] = []; - componentFixture.componentRef.setInput('competencies', competencies); - componentFixture.componentRef.setInput('relations', relations); - componentFixture.detectChanges(); - - component.tailCompetencyId = 17; - component.headCompetencyId = 16; - component.relationType = CompetencyRelationType.ASSUMES; - - component.validate(); - - expect(component.relationError).toBeUndefined(); - }); - - it('should prevent creating already existing relations', () => { - const competencies = [createCompetency(16, '16'), createCompetency(17, '17')]; - const relations = [createRelation(1, 16, 17, CompetencyRelationType.EXTENDS)]; - componentFixture.componentRef.setInput('competencies', competencies); - componentFixture.componentRef.setInput('relations', relations); - componentFixture.detectChanges(); - - component.tailCompetencyId = 16; - component.headCompetencyId = 17; - component.relationType = CompetencyRelationType.EXTENDS; - - component.validate(); - - expect(component.relationError).toBe(CompetencyRelationError.EXISTING); - }); - - it('should prevent creating self relations', () => { - const competencies = [createCompetency(16, '16'), createCompetency(17, '17')]; - const relations: CompetencyRelation[] = []; - componentFixture.componentRef.setInput('competencies', competencies); - componentFixture.componentRef.setInput('relations', relations); - componentFixture.detectChanges(); - - component.tailCompetencyId = 16; - component.headCompetencyId = 16; - component.relationType = CompetencyRelationType.EXTENDS; - - component.validate(); - - expect(component.relationError).toBe(CompetencyRelationError.SELF); - }); - - it('should zoom to fit and center on centerView', () => { - const zoomToFitStub = jest.spyOn(component.zoomToFit$, 'next'); - const centerStub = jest.spyOn(component.center$, 'next'); - componentFixture.detectChanges(); - component.centerView(); - expect(zoomToFitStub).toHaveBeenCalledExactlyOnceWith({ autoCenter: true }); - expect(centerStub).toHaveBeenCalledExactlyOnceWith(true); - }); - - function createCompetency(id: number, title: string) { - const competency: Competency = { - id: id, - title: title, - }; - return competency; - } - - function createRelation(id: number, tailCompetencyId: number, headCompetencyId: number, relationType: CompetencyRelationType) { - const relation: CompetencyRelation = { - id: id, - tailCompetency: { id: tailCompetencyId }, - headCompetency: { id: headCompetencyId }, - type: relationType, - }; - return relation; - } -}); diff --git a/src/test/javascript/spec/component/competencies/components/course-competencies-relation-graph.component.spec.ts b/src/test/javascript/spec/component/competencies/components/course-competencies-relation-graph.component.spec.ts new file mode 100644 index 000000000000..561f3ae5bd07 --- /dev/null +++ b/src/test/javascript/spec/component/competencies/components/course-competencies-relation-graph.component.spec.ts @@ -0,0 +1,93 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { CourseCompetenciesRelationGraphComponent } from 'app/course/competencies/components/course-competencies-relation-graph/course-competencies-relation-graph.component'; +import { CompetencyRelationDTO, CompetencyRelationType, CourseCompetency, CourseCompetencyType } from 'app/entities/competency.model'; +import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; +import { TranslateService } from '@ngx-translate/core'; + +describe('CourseCompetenciesRelationGraphComponent', () => { + let component: CourseCompetenciesRelationGraphComponent; + let fixture: ComponentFixture; + + const courseCompetencies: CourseCompetency[] = [ + { id: 1, type: CourseCompetencyType.COMPETENCY, title: 'Competency' }, + { id: 2, type: CourseCompetencyType.PREREQUISITE, title: 'Prerequisite' }, + ]; + + const relations: CompetencyRelationDTO[] = [ + { + id: 1, + relationType: CompetencyRelationType.EXTENDS, + tailCompetencyId: 1, + headCompetencyId: 2, + }, + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CourseCompetenciesRelationGraphComponent, NoopAnimationsModule], + providers: [ + { + provide: TranslateService, + useClass: MockTranslateService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(CourseCompetenciesRelationGraphComponent); + component = fixture.componentInstance; + + fixture.componentRef.setInput('courseCompetencies', courseCompetencies); + fixture.componentRef.setInput('relations', relations); + fixture.componentRef.setInput('selectedRelationId', 1); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should initialize', async () => { + expect(component).toBeDefined(); + expect(component.courseCompetencies()).toEqual(courseCompetencies); + expect(component.relations()).toEqual(relations); + }); + + it('should map edges correctly', () => { + expect(component.edges()).toEqual( + relations.map((relation) => { + return { + id: 'edge-' + relation.id!.toString(), + source: relation.headCompetencyId!.toString(), + target: relation.tailCompetencyId!.toString(), + label: relation.relationType, + data: { + id: relation.id, + }, + }; + }), + ); + }); + + it('should map nodes correctly', () => { + fixture.detectChanges(); + + expect(component.nodes()).toEqual( + courseCompetencies.map((cc) => { + return { + id: cc.id!.toString(), + label: cc.title, + data: { + id: cc.id, + type: cc.type, + }, + }; + }), + ); + }); + + it('should update node dimension', () => { + fixture.detectChanges(); + component['setNodeDimension']({ id: '1', dimension: { width: 0, height: 45.59 } }); + expect(component.nodes().find((node) => node.id === '1')?.dimension).toEqual({ width: 0, height: 45.59 }); + }); +}); diff --git a/src/test/javascript/spec/component/competencies/components/course-competencies-relation-modal.component.spec.ts b/src/test/javascript/spec/component/competencies/components/course-competencies-relation-modal.component.spec.ts new file mode 100644 index 000000000000..a3a99fd7a25e --- /dev/null +++ b/src/test/javascript/spec/component/competencies/components/course-competencies-relation-modal.component.spec.ts @@ -0,0 +1,129 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CourseCompetenciesRelationModalComponent } from 'app/course/competencies/components/course-competencies-relation-modal/course-competencies-relation-modal.component'; +import { CourseCompetencyApiService } from 'app/course/competencies/services/course-competency-api.service'; +import { AlertService } from 'app/core/util/alert.service'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { MockAlertService } from '../../../helpers/mocks/service/mock-alert.service'; +import { CompetencyRelationDTO, CompetencyRelationType, CourseCompetency, CourseCompetencyType } from 'app/entities/competency.model'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { MockNgbActiveModalService } from '../../../helpers/mocks/service/mock-ngb-active-modal.service'; +import { TranslateService } from '@ngx-translate/core'; +import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +describe('CourseCompetenciesRelationModalComponent', () => { + let component: CourseCompetenciesRelationModalComponent; + let fixture: ComponentFixture; + let courseCompetencyApiService: CourseCompetencyApiService; + let alertService: AlertService; + let activeModal: NgbActiveModal; + + const courseId = 1; + const courseCompetencies: CourseCompetency[] = [ + { id: 1, type: CourseCompetencyType.COMPETENCY }, + { id: 2, type: CourseCompetencyType.PREREQUISITE }, + ]; + const relations: CompetencyRelationDTO[] = [ + { + id: 1, + relationType: CompetencyRelationType.EXTENDS, + tailCompetencyId: 1, + headCompetencyId: 2, + }, + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CourseCompetenciesRelationModalComponent, NoopAnimationsModule], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + { + provide: NgbActiveModal, + useClass: MockNgbActiveModalService, + }, + { + provide: TranslateService, + useClass: MockTranslateService, + }, + { + provide: AlertService, + useClass: MockAlertService, + }, + { + provide: CourseCompetencyApiService, + useValue: { + getCourseCompetencyRelationsByCourseId: jest.fn(), + }, + }, + ], + }).compileComponents(); + + courseCompetencyApiService = TestBed.inject(CourseCompetencyApiService); + alertService = TestBed.inject(AlertService); + activeModal = TestBed.inject(NgbActiveModal); + + fixture = TestBed.createComponent(CourseCompetenciesRelationModalComponent); + component = fixture.componentInstance; + + jest.spyOn(courseCompetencyApiService, 'getCourseCompetencyRelationsByCourseId').mockResolvedValue(relations); + + fixture.componentRef.setInput('courseId', courseId); + fixture.componentRef.setInput('courseCompetencies', courseCompetencies); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should initialize', () => { + expect(component).toBeTruthy(); + }); + + it('should load relations', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.relations()).toEqual(relations); + }); + + it('should show alert on error', async () => { + const errorSpy = jest.spyOn(alertService, 'addAlert'); + jest.spyOn(courseCompetencyApiService, 'getCourseCompetencyRelationsByCourseId').mockReturnValue(Promise.reject(new Error('Error'))); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(errorSpy).toHaveBeenCalledOnce(); + }); + + it('should set isLoading correctly', async () => { + const isLoadingSpy = jest.spyOn(component.isLoading, 'set'); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(isLoadingSpy).toHaveBeenNthCalledWith(1, true); + expect(isLoadingSpy).toHaveBeenNthCalledWith(2, false); + }); + + it('should closeModal', () => { + const closeSpy = jest.spyOn(activeModal, 'close'); + + component['closeModal'](); + + expect(closeSpy).toHaveBeenCalledOnce(); + }); + + it('should call selectCourseCompetency on courseCompetencyRelationFormComponent with valid courseCompetencyId', () => { + fixture.detectChanges(); + + const courseCompetencyId = 1; + const selectSpy = jest.spyOn(component['courseCompetencyRelationFormComponent'](), 'selectCourseCompetency'); + + component['selectCourseCompetency'](courseCompetencyId); + + expect(selectSpy).toHaveBeenCalledExactlyOnceWith(courseCompetencyId); + }); +}); diff --git a/src/test/javascript/spec/component/competencies/components/course-competency-relation-form.component.spec.ts b/src/test/javascript/spec/component/competencies/components/course-competency-relation-form.component.spec.ts new file mode 100644 index 000000000000..aed1e03612f8 --- /dev/null +++ b/src/test/javascript/spec/component/competencies/components/course-competency-relation-form.component.spec.ts @@ -0,0 +1,364 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CourseCompetencyRelationFormComponent, UnionFind } from 'app/course/competencies/components/course-competency-relation-form/course-competency-relation-form.component'; +import { AlertService } from 'app/core/util/alert.service'; +import { MockAlertService } from '../../../helpers/mocks/service/mock-alert.service'; +import { CourseCompetencyApiService } from 'app/course/competencies/services/course-competency-api.service'; +import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; +import { TranslateService } from '@ngx-translate/core'; +import { CompetencyRelationDTO, CompetencyRelationType, CourseCompetency, UpdateCourseCompetencyRelationDTO } from 'app/entities/competency.model'; + +describe('CourseCompetencyRelationFormComponent', () => { + let component: CourseCompetencyRelationFormComponent; + let fixture: ComponentFixture; + let courseCompetencyApiService: CourseCompetencyApiService; + let alertService: AlertService; + + let createCourseCompetencyRelationSpy: jest.SpyInstance; + let updateCourseCompetencyRelationSpy: jest.SpyInstance; + let deleteCourseCompetencyRelationSpy: jest.SpyInstance; + + const courseId = 1; + const courseCompetencies: CourseCompetency[] = [ + { id: 1, title: 'Competency 1' }, + { id: 2, title: 'Competency 2' }, + { id: 3, title: 'Competency 3' }, + ]; + const relations: CompetencyRelationDTO[] = [ + { + id: 1, + tailCompetencyId: 1, + headCompetencyId: 2, + relationType: CompetencyRelationType.EXTENDS, + }, + ]; + const selectedRelationId = 1; + + const newRelation = { + id: 2, + headCompetencyId: 2, + tailCompetencyId: 3, + relationType: CompetencyRelationType.EXTENDS, + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CourseCompetencyRelationFormComponent], + providers: [ + { + provide: AlertService, + useClass: MockAlertService, + }, + { + provide: TranslateService, + useClass: MockTranslateService, + }, + { + provide: CourseCompetencyApiService, + useValue: { + createCourseCompetencyRelation: jest.fn(), + updateCourseCompetencyRelation: jest.fn(), + deleteCourseCompetencyRelation: jest.fn(), + }, + }, + ], + }).compileComponents(); + + courseCompetencyApiService = TestBed.inject(CourseCompetencyApiService); + alertService = TestBed.inject(AlertService); + + createCourseCompetencyRelationSpy = jest.spyOn(courseCompetencyApiService, 'createCourseCompetencyRelation').mockResolvedValue(newRelation); + updateCourseCompetencyRelationSpy = jest.spyOn(courseCompetencyApiService, 'updateCourseCompetencyRelation').mockResolvedValue(); + deleteCourseCompetencyRelationSpy = jest.spyOn(courseCompetencyApiService, 'deleteCourseCompetencyRelation').mockResolvedValue(); + + fixture = TestBed.createComponent(CourseCompetencyRelationFormComponent); + component = fixture.componentInstance; + + fixture.componentRef.setInput('courseId', courseId); + fixture.componentRef.setInput('courseCompetencies', courseCompetencies); + fixture.componentRef.setInput('relations', relations); + fixture.componentRef.setInput('selectedRelationId', undefined); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should set relationAlreadyExists correctly', () => { + component.headCompetencyId.set(2); + component.tailCompetencyId.set(1); + component.relationType.set(CompetencyRelationType.ASSUMES); + + fixture.detectChanges(); + + expect(component.relationAlreadyExists()).toBeTrue(); + }); + + it('should set exactRelationAlreadyExists correctly', () => { + component.headCompetencyId.set(2); + component.tailCompetencyId.set(1); + component.relationType.set(CompetencyRelationType.EXTENDS); + + fixture.detectChanges(); + + expect(component.exactRelationAlreadyExists()).toBeTrue(); + }); + + it('should select relation if selectedRelationId is set', () => { + fixture.componentRef.setInput('selectedRelationId', selectedRelationId); + + fixture.detectChanges(); + + expect(component.headCompetencyId()).toBe(2); + expect(component.tailCompetencyId()).toBe(1); + expect(component.relationType()).toBe(CompetencyRelationType.EXTENDS); + }); + + it('should set headCompetencyId if it is undefined', () => { + component.headCompetencyId.set(undefined); + component.tailCompetencyId.set(2); + + component.selectCourseCompetency(1); + + expect(component.headCompetencyId()).toBe(1); + expect(component.tailCompetencyId()).toBeUndefined(); + }); + + it('should set tailCompetencyId if headCompetencyId is defined and tailCompetencyId is undefined', () => { + component.headCompetencyId.set(1); + component.tailCompetencyId.set(undefined); + + component.selectCourseCompetency(2); + + expect(component.tailCompetencyId()).toBe(2); + }); + + it('should reset headCompetencyId if both headCompetencyId and tailCompetencyId are defined', () => { + component.headCompetencyId.set(1); + component.tailCompetencyId.set(2); + + component.selectCourseCompetency(3); + + expect(component.headCompetencyId()).toBe(3); + expect(component.tailCompetencyId()).toBeUndefined(); + }); + + it('should create relation', async () => { + component.headCompetencyId.set(2); + component.tailCompetencyId.set(3); + component.relationType.set(CompetencyRelationType.EXTENDS); + + await component['createRelation'](); + + expect(createCourseCompetencyRelationSpy).toHaveBeenCalledExactlyOnceWith(courseId, { + headCompetencyId: 2, + tailCompetencyId: 3, + relationType: CompetencyRelationType.EXTENDS, + }); + expect(component.headCompetencyId()).toBe(2); + expect(component.tailCompetencyId()).toBe(3); + expect(component.relationType()).toBe(CompetencyRelationType.EXTENDS); + expect(component.selectedRelationId()).toBe(2); + expect(component.relations()).toEqual([...relations, newRelation]); + }); + + it('should set isLoading correctly when creating a relation', async () => { + const isLoadingSpy = jest.spyOn(component.isLoading, 'set'); + + component.headCompetencyId.set(2); + component.tailCompetencyId.set(3); + component.relationType.set(CompetencyRelationType.EXTENDS); + + await component['createRelation'](); + + expect(isLoadingSpy).toHaveBeenNthCalledWith(1, true); + expect(isLoadingSpy).toHaveBeenNthCalledWith(2, false); + }); + + it('should show error when creating relation fails', async () => { + const error = 'Error creating relation'; + createCourseCompetencyRelationSpy.mockRejectedValue(error); + const alertServiceErrorSpy = jest.spyOn(alertService, 'error'); + + component.headCompetencyId.set(2); + component.tailCompetencyId.set(3); + component.relationType.set(CompetencyRelationType.EXTENDS); + + await component['createRelation'](); + + expect(alertServiceErrorSpy).toHaveBeenCalledOnce(); + }); + + it('should update relation', async () => { + fixture.componentRef.setInput('selectedRelationId', selectedRelationId); + + fixture.detectChanges(); + + component.relationType.set(CompetencyRelationType.ASSUMES); + + await component['updateRelation'](); + + expect(updateCourseCompetencyRelationSpy).toHaveBeenCalledExactlyOnceWith(courseId, selectedRelationId, { + newRelationType: CompetencyRelationType.ASSUMES, + }); + const newRelations = [...relations].map((relation) => { + if (relation.id === selectedRelationId) { + return { ...relation, relationType: CompetencyRelationType.ASSUMES }; + } + return relation; + }); + expect(component.relations()).toEqual(newRelations); + }); + + it('should set isLoading correctly when updating a relation', async () => { + const isLoadingSpy = jest.spyOn(component.isLoading, 'set'); + fixture.componentRef.setInput('selectedRelationId', selectedRelationId); + + fixture.detectChanges(); + + component.relationType.set(CompetencyRelationType.ASSUMES); + + await component['updateRelation'](); + + expect(isLoadingSpy).toHaveBeenNthCalledWith(1, true); + expect(isLoadingSpy).toHaveBeenNthCalledWith(2, false); + }); + + it('should show error when updating relation fails', async () => { + updateCourseCompetencyRelationSpy.mockRejectedValue('Error updating relation'); + const alertServiceErrorSpy = jest.spyOn(alertService, 'error'); + fixture.componentRef.setInput('selectedRelationId', selectedRelationId); + + fixture.detectChanges(); + + component.relationType.set(CompetencyRelationType.ASSUMES); + + await component['updateRelation'](); + + expect(alertServiceErrorSpy).toHaveBeenCalledOnce(); + }); + + it('should select head course competency', () => { + component['selectHeadCourseCompetency'](2); + + expect(component.headCompetencyId()).toBe(2); + expect(component.tailCompetencyId()).toBeUndefined(); + expect(component.selectedRelationId()).toBeUndefined(); + }); + + it('should set tailCompetencyId and selectedRelationId when an existing relation is found', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + const tailId = 1; + component.headCompetencyId.set(2); + component.relationType.set(CompetencyRelationType.EXTENDS); + + component['selectTailCourseCompetency'](tailId); + + expect(component.tailCompetencyId()).toBe(1); + expect(component.selectedRelationId()).toBe(1); + }); + + it('should set tailCompetencyId and clear selectedRelationId when no existing relation is found', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + const tailId = 2; + component.headCompetencyId.set(3); + component.relationType.set(CompetencyRelationType.EXTENDS); + + component['selectTailCourseCompetency'](tailId); + + expect(component.tailCompetencyId()).toBe(2); + expect(component.selectedRelationId()).toBeUndefined(); + }); + + it('should not allow to create circular dependencies', () => { + component.headCompetencyId.set(1); + component.tailCompetencyId.set(1); + component.relationType.set(CompetencyRelationType.EXTENDS); + + expect(component['selectableTailCourseCompetencyIds']).not.toContain(1); + expect(component.showCircularDependencyError()).toBeTrue(); + }); + + it('should delete relation', async () => { + fixture.componentRef.setInput('selectedRelationId', selectedRelationId); + + fixture.detectChanges(); + + await component['deleteRelation'](); + + expect(deleteCourseCompetencyRelationSpy).toHaveBeenCalledExactlyOnceWith(courseId, selectedRelationId); + expect(component.relations()).toHaveLength(relations.length - 1); + }); + + it('should set isLoading correctly when deleting a relation', async () => { + const isLoadingSpy = jest.spyOn(component.isLoading, 'set'); + fixture.componentRef.setInput('selectedRelationId', selectedRelationId); + + fixture.detectChanges(); + + await component['deleteRelation'](); + + expect(isLoadingSpy).toHaveBeenNthCalledWith(1, true); + expect(isLoadingSpy).toHaveBeenNthCalledWith(2, false); + }); + + it('should show error when deleting relation fails', async () => { + deleteCourseCompetencyRelationSpy.mockRejectedValue('Error deleting relation'); + const alertServiceErrorSpy = jest.spyOn(alertService, 'error'); + fixture.componentRef.setInput('selectedRelationId', selectedRelationId); + + fixture.detectChanges(); + + await component['deleteRelation'](); + + expect(alertServiceErrorSpy).toHaveBeenCalledOnce(); + }); +}); + +describe('UnionFind', () => { + let unionFind: UnionFind; + + beforeEach(() => { + unionFind = new UnionFind(5); + }); + + it('should initialize parent and rank arrays correctly', () => { + expect(unionFind.parent).toEqual([0, 1, 2, 3, 4]); + expect(unionFind.rank).toEqual([1, 1, 1, 1, 1]); + }); + + it('should find the representative of a set', () => { + expect(unionFind.find(0)).toBe(0); + expect(unionFind.find(1)).toBe(1); + }); + + it('should perform union by rank correctly', () => { + unionFind.union(0, 1); + expect(unionFind.find(0)).toBe(unionFind.find(1)); + }); + + it('should perform path compression correctly', () => { + unionFind.union(0, 1); + unionFind.union(1, 2); + expect(unionFind.find(2)).toBe(0); + expect(unionFind.parent[2]).toBe(0); + }); + + it('should handle union of already connected components', () => { + unionFind.union(0, 1); + unionFind.union(1, 2); + unionFind.union(0, 2); + expect(unionFind.find(2)).toBe(0); + }); + + it('should handle union of components with equal rank', () => { + unionFind.union(0, 1); + unionFind.union(2, 3); + unionFind.union(1, 2); + expect(unionFind.find(3)).toBe(0); + expect(unionFind.rank[0]).toBe(3); // Corrected expected rank value + }); +}); diff --git a/src/test/javascript/spec/component/competencies/components/course-competency-relation-node.component.spec.ts b/src/test/javascript/spec/component/competencies/components/course-competency-relation-node.component.spec.ts new file mode 100644 index 000000000000..a44fb57502e2 --- /dev/null +++ b/src/test/javascript/spec/component/competencies/components/course-competency-relation-node.component.spec.ts @@ -0,0 +1,50 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CourseCompetencyType } from 'app/entities/competency.model'; +import { Node } from '@swimlane/ngx-graph'; +import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; +import { TranslateService } from '@ngx-translate/core'; +import { CourseCompetencyRelationNodeComponent } from 'app/course/competencies/components/course-competency-relation-node/course-competency-relation-node.component'; + +describe('CourseCompetencyRelationNodeComponent', () => { + let component: CourseCompetencyRelationNodeComponent; + let fixture: ComponentFixture; + + const node: Node = { + id: '1', + label: 'Competency 1', + data: { + type: CourseCompetencyType.COMPETENCY, + }, + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CourseCompetencyRelationNodeComponent], + providers: [ + { + provide: TranslateService, + useClass: MockTranslateService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(CourseCompetencyRelationNodeComponent); + component = fixture.componentInstance; + + fixture.componentRef.setInput('courseCompetencyNode', node); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should initialize and emit size update', async () => { + const sizeUpdateEmitSpy = jest.spyOn(component.onSizeSet, 'emit'); + + fixture.detectChanges(); + + expect(component).toBeTruthy(); + expect(component.courseCompetencyNode()).toEqual(node); + expect(sizeUpdateEmitSpy).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/test/javascript/spec/component/competencies/components/import-course-competencies-modal/import-all-course-competencies-modal.component.spec.ts b/src/test/javascript/spec/component/competencies/components/import-all-course-competencies-modal.component.spec.ts similarity index 96% rename from src/test/javascript/spec/component/competencies/components/import-course-competencies-modal/import-all-course-competencies-modal.component.spec.ts rename to src/test/javascript/spec/component/competencies/components/import-all-course-competencies-modal.component.spec.ts index eb4ecd81c175..40e81a50cf6f 100644 --- a/src/test/javascript/spec/component/competencies/components/import-course-competencies-modal/import-all-course-competencies-modal.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/components/import-all-course-competencies-modal.component.spec.ts @@ -2,7 +2,7 @@ import '@angular/localize/init'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ImportAllCourseCompetenciesModalComponent } from 'app/course/competencies/components/import-all-course-competencies-modal/import-all-course-competencies-modal.component'; -import { MockTranslateService } from '../../../../helpers/mocks/service/mock-translate.service'; +import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; import { TranslateService } from '@ngx-translate/core'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; @@ -71,7 +71,7 @@ describe('ImportAllCourseCompetenciesModalComponent', () => { const course = { id: 2, title: 'Course 02', shortName: 'C2' }; component.selectCourse(course); - expect(closeModalSpy).toHaveBeenCalledWith({ + expect(closeModalSpy).toHaveBeenCalledExactlyOnceWith({ course: course, courseCompetencyImportOptions: { sourceCourseId: course.id, diff --git a/src/test/javascript/spec/component/competencies/components/import-course-competencies-settings/import-course-competencies-settings.component.spec.ts b/src/test/javascript/spec/component/competencies/components/import-course-competencies-settings.component.spec.ts similarity index 98% rename from src/test/javascript/spec/component/competencies/components/import-course-competencies-settings/import-course-competencies-settings.component.spec.ts rename to src/test/javascript/spec/component/competencies/components/import-course-competencies-settings.component.spec.ts index 1d9d6356c01a..3777ba7b1a3d 100644 --- a/src/test/javascript/spec/component/competencies/components/import-course-competencies-settings/import-course-competencies-settings.component.spec.ts +++ b/src/test/javascript/spec/component/competencies/components/import-course-competencies-settings.component.spec.ts @@ -3,7 +3,7 @@ import { CourseCompetencyImportSettings, ImportCourseCompetenciesSettingsComponent, } from 'app/course/competencies/components/import-course-competencies-settings/import-course-competencies-settings.component'; -import { MockTranslateService } from '../../../../helpers/mocks/service/mock-translate.service'; +import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; import { TranslateService } from '@ngx-translate/core'; describe('ImportCourseCompetenciesSettingsComponent', () => { diff --git a/src/test/javascript/spec/service/course-competency/course-competency-api.service.spec.ts b/src/test/javascript/spec/service/course-competency/course-competency-api.service.spec.ts index e641f31f9ae7..fc33eb3fff62 100644 --- a/src/test/javascript/spec/service/course-competency/course-competency-api.service.spec.ts +++ b/src/test/javascript/spec/service/course-competency/course-competency-api.service.spec.ts @@ -2,7 +2,7 @@ import { provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; import { CourseCompetencyApiService } from 'app/course/competencies/services/course-competency-api.service'; -import { CompetencyRelation, CourseCompetencyImportOptionsDTO } from 'app/entities/competency.model'; +import { CompetencyRelation, CompetencyRelationType, CourseCompetencyImportOptionsDTO, UpdateCourseCompetencyRelationDTO } from 'app/entities/competency.model'; describe('CourseCompetencyApiService', () => { let httpClient: HttpTestingController; @@ -42,6 +42,18 @@ describe('CourseCompetencyApiService', () => { await methodCall; }); + it('should update course competency relation', async () => { + const relationId = 1; + const relationType = CompetencyRelationType.EXTENDS; + const methodCall = courseCompetencyApiService.updateCourseCompetencyRelation(courseId, relationId, { newRelationType: relationType }); + const response = httpClient.expectOne({ + method: 'PATCH', + url: `${baseUrl}/courses/${courseId}/course-competencies/relations/${relationId}`, + }); + response.flush({}); + await methodCall; + }); + it('should create course competency relation', async () => { const relation = { tailCompetencyId: 1, From bdf7396d0932320c38d4ea43c826e8114379a57f Mon Sep 17 00:00:00 2001 From: Ole Vester <73833780+ole-ve@users.noreply.github.com> Date: Tue, 22 Oct 2024 23:35:34 +0200 Subject: [PATCH 02/18] Development: Refactor programming tests (#9506) --- .../AtlasTestArchitectureTest.java | 8 +- .../LearningPathIntegrationTest.java | 10 +- .../AbstractFileUploadIntegrationTest.java | 2 +- .../FileUploadTestArchitectureTest.java | 8 +- .../architecture/LtiTestArchitectureTest.java | 8 +- ...mingIntegrationGitlabCIGitlabSamlTest.java | 82 +++++++ ...ProgrammingIntegrationIndependentTest.java | 183 +++++++++++++++ ...ogrammingIntegrationJenkinsGitlabTest.java | 209 ++++++++++++++++++ ...grammingIntegrationLocalCILocalVCTest.java | 103 +++++++++ ...ingIntegrationLocalCILocalVCTestBase.java} | 111 ++++++++-- .../AuxiliaryRepositoryServiceTest.java | 24 +- .../programming/BuildPlanIntegrationTest.java | 20 +- ...encyCheckGitlabJenkinsIntegrationTest.java | 9 +- .../CourseGitlabJenkinsIntegrationTest.java | 21 +- .../artemis/programming/GitServiceTest.java | 7 +- .../programming/GitlabServiceTest.java | 21 +- .../IdePreferencesIntegrationTest.java | 12 +- .../programming/PlantUmlIntegrationTest.java | 3 +- .../ProgrammingAssessmentIntegrationTest.java | 50 +---- .../ProgrammingExerciseBuildPlanTest.java | 16 +- ...ProgrammingExerciseGitIntegrationTest.java | 20 +- ...gExerciseGitlabJenkinsIntegrationTest.java | 12 +- ...ProgrammingExerciseGradingServiceTest.java | 55 +---- ...gExerciseIntegrationJenkinsGitlabTest.java | 11 +- ...rammingExerciseIntegrationTestService.java | 2 +- ...gExerciseParticipationIntegrationTest.java | 36 +-- ...grammingExerciseRepositoryServiceTest.java | 28 +-- ...gExerciseResultJenkinsIntegrationTest.java | 8 +- ...rogrammingExerciseScheduleServiceTest.java | 48 +--- ...rammingExerciseServiceIntegrationTest.java | 24 +- .../ProgrammingExerciseServiceTest.java | 25 +-- ...ammingExerciseTemplateIntegrationTest.java | 12 +- .../programming/ProgrammingExerciseTest.java | 42 +--- ...rogrammingExerciseTestCaseServiceTest.java | 36 +-- ...AndResultGitlabJenkinsIntegrationTest.java | 36 +-- .../ProgrammingSubmissionIntegrationTest.java | 47 +--- .../RepositoryIntegrationTest.java | 60 +---- ...seParticipationJenkinsIntegrationTest.java | 16 +- .../StaticCodeAnalysisIntegrationTest.java | 24 +- .../SubmissionPolicyIntegrationTest.java | 20 +- ...TestRepositoryResourceIntegrationTest.java | 12 +- .../ProgrammingTestArchitectureTest.java | 31 +++ .../hestia/CodeHintIntegrationTest.java | 23 +- .../hestia/CodeHintServiceTest.java | 55 +---- .../hestia/ExerciseHintIntegrationTest.java | 42 +--- .../hestia/ExerciseHintServiceTest.java | 69 +----- .../hestia/HestiaDatabaseTest.java | 73 ++---- ...gExerciseGitDiffReportIntegrationTest.java | 13 +- ...mmingExerciseGitDiffReportServiceTest.java | 29 +-- ...gExerciseSolutionEntryIntegrationTest.java | 55 ++--- ...rogrammingExerciseTaskIntegrationTest.java | 39 +--- .../ProgrammingExerciseTaskServiceTest.java | 112 ++++------ .../hestia/StructuralTestCaseServiceTest.java | 25 +-- .../TestwiseCoverageIntegrationTest.java | 39 +--- .../TestwiseCoverageReportServiceTest.java | 47 +--- ...ralTestCaseServiceLocalCILocalVCTest.java} | 53 +---- .../icl/LocalCIIntegrationTest.java | 33 +-- .../icl/LocalCIResourceIntegrationTest.java | 22 +- .../icl/LocalCIResultServiceTest.java | 8 +- .../programming/icl/LocalCIServiceTest.java | 35 +-- .../icl/LocalVCIntegrationTest.java | 3 +- .../icl/LocalVCLocalCIIntegrationTest.java | 36 +-- ...VCLocalCIParticipationIntegrationTest.java | 17 +- .../programming/icl/LocalVCServiceTest.java | 17 +- .../icl/LocalVCSshIntegrationTest.java | 5 - .../icl/MultipleHostKeyProviderTest.java | 3 +- ...ExerciseLocalVCLocalCIIntegrationTest.java | 28 +-- .../icl/SharedQueueManagementServiceTest.java | 15 +- .../service/BuildLogEntryServiceTest.java | 8 +- .../service/GitlabCIServiceTest.java | 50 +---- .../JenkinsAuthorizationInterceptorTest.java | 18 +- .../JenkinsInternalUriServiceTest.java | 9 +- .../JenkinsJobPermissionServiceTest.java | 9 +- .../service/JenkinsJobServiceTest.java | 13 +- .../service/JenkinsServiceTest.java | 36 +-- ...ngExerciseFeedbackCreationServiceTest.java | 28 +-- .../service/RepositoryAccessServiceTest.java | 31 +-- ...sonalAccessTokenManagementServiceTest.java | 17 +- .../JenkinsPipelineScriptCreatorTest.java | 25 +-- .../AbstractModuleTestArchitectureTest.java | 72 +++++- .../TutorialGroupTestArchitectureTest.java | 8 +- 81 files changed, 1000 insertions(+), 1732 deletions(-) create mode 100644 src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationGitlabCIGitlabSamlTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationIndependentTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationJenkinsGitlabTest.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTest.java rename src/test/java/de/tum/cit/aet/artemis/programming/{icl/AbstractLocalCILocalVCIntegrationTest.java => AbstractProgrammingIntegrationLocalCILocalVCTestBase.java} (63%) create mode 100644 src/test/java/de/tum/cit/aet/artemis/programming/architecture/ProgrammingTestArchitectureTest.java rename src/test/java/de/tum/cit/aet/artemis/programming/hestia/behavioral/{BehavioralTestCaseServiceTest.java => BehavioralTestCaseServiceLocalCILocalVCTest.java} (74%) diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/architecture/AtlasTestArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/architecture/AtlasTestArchitectureTest.java index 96bdd6b27d58..ff479619c209 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/architecture/AtlasTestArchitectureTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/architecture/AtlasTestArchitectureTest.java @@ -1,9 +1,11 @@ package de.tum.cit.aet.artemis.atlas.architecture; +import java.util.Set; + import de.tum.cit.aet.artemis.atlas.AbstractAtlasIntegrationTest; import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleTestArchitectureTest; -class AtlasTestArchitectureTest extends AbstractModuleTestArchitectureTest { +class AtlasTestArchitectureTest extends AbstractModuleTestArchitectureTest { @Override public String getModulePackage() { @@ -11,7 +13,7 @@ public String getModulePackage() { } @Override - protected Class getAbstractModuleIntegrationTestClass() { - return AbstractAtlasIntegrationTest.class; + protected Set> getAbstractModuleIntegrationTestClasses() { + return Set.of(AbstractAtlasIntegrationTest.class); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/LearningPathIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/LearningPathIntegrationTest.java index b217564b469f..7ec124ecc537 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/LearningPathIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/learningpath/LearningPathIntegrationTest.java @@ -379,7 +379,7 @@ void testUpdateLearningPathProgress() throws Exception { * This only tests if the end point successfully retrieves the health status. The correctness of the health status is tested in LearningPathServiceTest. * * @throws Exception the request failed - * @see de.tum.cit.aet.artemis.service.LearningPathServiceTest + * @see de.tum.cit.aet.artemis.atlas.service.LearningPathServiceTest */ @Test @WithMockUser(username = INSTRUCTOR_OF_COURSE, roles = "INSTRUCTOR") @@ -468,7 +468,7 @@ void testGetLearningPathNgxForOtherStudent(LearningPathResource.NgxRequestType t * This only tests if the end point successfully retrieves the graph representation. The correctness of the response is tested in LearningPathServiceTest. * * @throws Exception the request failed - * @see de.tum.cit.aet.artemis.service.LearningPathServiceTest + * @see de.tum.cit.aet.artemis.atlas.service.LearningPathServiceTest */ @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @EnumSource(LearningPathResource.NgxRequestType.class) @@ -484,7 +484,7 @@ void testGetLearningPathNgxAsStudent(LearningPathResource.NgxRequestType type) t * This only tests if the end point successfully retrieves the graph representation. The correctness of the response is tested in LearningPathServiceTest. * * @throws Exception the request failed - * @see de.tum.cit.aet.artemis.service.LearningPathServiceTest + * @see de.tum.cit.aet.artemis.atlas.service.LearningPathServiceTest */ @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @EnumSource(LearningPathResource.NgxRequestType.class) @@ -500,7 +500,7 @@ void testGetLearningPathNgxAsTutor(LearningPathResource.NgxRequestType type) thr * This only tests if the end point successfully retrieves the graph representation. The correctness of the response is tested in LearningPathServiceTest. * * @throws Exception the request failed - * @see de.tum.cit.aet.artemis.service.LearningPathServiceTest + * @see de.tum.cit.aet.artemis.atlas.service.LearningPathServiceTest */ @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @EnumSource(LearningPathResource.NgxRequestType.class) @@ -516,7 +516,7 @@ void testGetLearningPathNgxAsEditor(LearningPathResource.NgxRequestType type) th * This only tests if the end point successfully retrieves the graph representation. The correctness of the response is tested in LearningPathServiceTest. * * @throws Exception the request failed - * @see de.tum.cit.aet.artemis.service.LearningPathServiceTest + * @see de.tum.cit.aet.artemis.atlas.service.LearningPathServiceTest */ @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @EnumSource(LearningPathResource.NgxRequestType.class) diff --git a/src/test/java/de/tum/cit/aet/artemis/fileupload/AbstractFileUploadIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/fileupload/AbstractFileUploadIntegrationTest.java index 6c6ff7719640..f85aa92027b7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/fileupload/AbstractFileUploadIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/fileupload/AbstractFileUploadIntegrationTest.java @@ -21,7 +21,7 @@ import de.tum.cit.aet.artemis.modeling.util.ModelingExerciseUtilService; import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -public class AbstractFileUploadIntegrationTest extends AbstractSpringIntegrationIndependentTest { +public abstract class AbstractFileUploadIntegrationTest extends AbstractSpringIntegrationIndependentTest { // Repositories @Autowired diff --git a/src/test/java/de/tum/cit/aet/artemis/fileupload/architecture/FileUploadTestArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/fileupload/architecture/FileUploadTestArchitectureTest.java index f67db93f311f..8e36ca4a74d1 100644 --- a/src/test/java/de/tum/cit/aet/artemis/fileupload/architecture/FileUploadTestArchitectureTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/fileupload/architecture/FileUploadTestArchitectureTest.java @@ -1,9 +1,11 @@ package de.tum.cit.aet.artemis.fileupload.architecture; +import java.util.Set; + import de.tum.cit.aet.artemis.fileupload.AbstractFileUploadIntegrationTest; import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleTestArchitectureTest; -class FileUploadTestArchitectureTest extends AbstractModuleTestArchitectureTest { +class FileUploadTestArchitectureTest extends AbstractModuleTestArchitectureTest { @Override public String getModulePackage() { @@ -11,7 +13,7 @@ public String getModulePackage() { } @Override - protected Class getAbstractModuleIntegrationTestClass() { - return AbstractFileUploadIntegrationTest.class; + protected Set> getAbstractModuleIntegrationTestClasses() { + return Set.of(AbstractFileUploadIntegrationTest.class); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/lti/architecture/LtiTestArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/lti/architecture/LtiTestArchitectureTest.java index ceb15210ada3..4d18fa50b636 100644 --- a/src/test/java/de/tum/cit/aet/artemis/lti/architecture/LtiTestArchitectureTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/lti/architecture/LtiTestArchitectureTest.java @@ -1,9 +1,11 @@ package de.tum.cit.aet.artemis.lti.architecture; +import java.util.Set; + import de.tum.cit.aet.artemis.lti.AbstractLtiIntegrationTest; import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleTestArchitectureTest; -class LtiTestArchitectureTest extends AbstractModuleTestArchitectureTest { +class LtiTestArchitectureTest extends AbstractModuleTestArchitectureTest { @Override public String getModulePackage() { @@ -11,7 +13,7 @@ public String getModulePackage() { } @Override - protected Class getAbstractModuleIntegrationTestClass() { - return AbstractLtiIntegrationTest.class; + protected Set> getAbstractModuleIntegrationTestClasses() { + return Set.of(AbstractLtiIntegrationTest.class); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationGitlabCIGitlabSamlTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationGitlabCIGitlabSamlTest.java new file mode 100644 index 000000000000..cb8866b2160a --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationGitlabCIGitlabSamlTest.java @@ -0,0 +1,82 @@ +package de.tum.cit.aet.artemis.programming; + +import java.net.URL; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import de.tum.cit.aet.artemis.core.service.messaging.InstanceMessageReceiveService; +import de.tum.cit.aet.artemis.core.user.util.UserUtilService; +import de.tum.cit.aet.artemis.exam.repository.ExamRepository; +import de.tum.cit.aet.artemis.exam.test_repository.StudentExamTestRepository; +import de.tum.cit.aet.artemis.exam.util.ExamUtilService; +import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; +import de.tum.cit.aet.artemis.exercise.test_repository.ParticipationTestRepository; +import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; +import de.tum.cit.aet.artemis.programming.repository.BuildLogStatisticsEntryRepository; +import de.tum.cit.aet.artemis.programming.repository.BuildPlanRepository; +import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; +import de.tum.cit.aet.artemis.programming.service.gitlabci.GitLabCIResultService; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; +import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; +import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationGitlabCIGitlabSamlTest; + +public abstract class AbstractProgrammingIntegrationGitlabCIGitlabSamlTest extends AbstractSpringIntegrationGitlabCIGitlabSamlTest { + + // Config + @Value("${artemis.version-control.url}") + protected URL gitlabServerUrl; + + // Repositories + @Autowired + protected BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository; + + @Autowired + protected BuildPlanRepository buildPlanRepository; + + @Autowired + protected ParticipationTestRepository participationRepository; + + @Autowired + protected ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + + @Autowired + protected ProgrammingExerciseTestCaseTestRepository programmingExerciseTestCaseRepository; + + @Autowired + protected ProgrammingExerciseTestRepository programmingExerciseRepository; + + // External Repositories + @Autowired + protected ExamRepository examRepository; + + @Autowired + protected StudentExamTestRepository studentExamRepository; + + // Services + @Autowired + protected GitLabCIResultService gitLabCIResultService; + + // External Services + @Autowired + protected InstanceMessageReceiveService instanceMessageReceiveService; + + // Util Services + @Autowired + protected ProgrammingExerciseUtilService programmingExerciseUtilService; + + // External Util Services + @Autowired + protected ExamUtilService examUtilService; + + @Autowired + protected ExerciseUtilService exerciseUtilService; + + @Autowired + protected ParticipationUtilService participationUtilService; + + @Autowired + protected UserUtilService userUtilService; + +} diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationIndependentTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationIndependentTest.java new file mode 100644 index 000000000000..46749fbcd3ec --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationIndependentTest.java @@ -0,0 +1,183 @@ +package de.tum.cit.aet.artemis.programming; + +import org.springframework.beans.factory.annotation.Autowired; + +import de.tum.cit.aet.artemis.assessment.repository.ComplaintRepository; +import de.tum.cit.aet.artemis.assessment.util.ComplaintUtilService; +import de.tum.cit.aet.artemis.core.test_repository.UserTestRepository; +import de.tum.cit.aet.artemis.core.user.util.UserUtilService; +import de.tum.cit.aet.artemis.core.util.CourseUtilService; +import de.tum.cit.aet.artemis.exam.repository.ExamRepository; +import de.tum.cit.aet.artemis.exam.util.ExamUtilService; +import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; +import de.tum.cit.aet.artemis.exercise.repository.ExerciseTestRepository; +import de.tum.cit.aet.artemis.exercise.test_repository.ParticipationTestRepository; +import de.tum.cit.aet.artemis.exercise.test_repository.StudentParticipationTestRepository; +import de.tum.cit.aet.artemis.exercise.test_repository.SubmissionTestRepository; +import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; +import de.tum.cit.aet.artemis.programming.repository.AuxiliaryRepositoryRepository; +import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; +import de.tum.cit.aet.artemis.programming.repository.SolutionProgrammingExerciseParticipationRepository; +import de.tum.cit.aet.artemis.programming.repository.StaticCodeAnalysisCategoryRepository; +import de.tum.cit.aet.artemis.programming.repository.hestia.CodeHintRepository; +import de.tum.cit.aet.artemis.programming.repository.hestia.CoverageFileReportRepository; +import de.tum.cit.aet.artemis.programming.repository.hestia.CoverageReportRepository; +import de.tum.cit.aet.artemis.programming.repository.hestia.ExerciseHintActivationRepository; +import de.tum.cit.aet.artemis.programming.repository.hestia.ExerciseHintRepository; +import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseSolutionEntryRepository; +import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository; +import de.tum.cit.aet.artemis.programming.repository.hestia.TestwiseCoverageReportEntryRepository; +import de.tum.cit.aet.artemis.programming.repository.settings.IdeRepository; +import de.tum.cit.aet.artemis.programming.repository.settings.UserIdeMappingRepository; +import de.tum.cit.aet.artemis.programming.service.AuxiliaryRepositoryService; +import de.tum.cit.aet.artemis.programming.service.BuildLogEntryService; +import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseFeedbackCreationService; +import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseGradingService; +import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseRepositoryService; +import de.tum.cit.aet.artemis.programming.service.hestia.CodeHintService; +import de.tum.cit.aet.artemis.programming.service.hestia.ExerciseHintService; +import de.tum.cit.aet.artemis.programming.service.hestia.ProgrammingExerciseTaskService; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseStudentParticipationTestRepository; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; +import de.tum.cit.aet.artemis.programming.util.GitUtilService; +import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; +import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; + +public abstract class AbstractProgrammingIntegrationIndependentTest extends AbstractSpringIntegrationIndependentTest { + + // Repositories + @Autowired + protected AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; + + @Autowired + protected CodeHintRepository codeHintRepository; + + @Autowired + protected CoverageFileReportRepository coverageFileReportRepository; + + @Autowired + protected CoverageReportRepository coverageReportRepository; + + @Autowired + protected ExerciseHintActivationRepository exerciseHintActivationRepository; + + @Autowired + protected ExerciseHintRepository exerciseHintRepository; + + @Autowired + protected IdeRepository ideRepository; + + @Autowired + protected ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + + @Autowired + protected ProgrammingExerciseSolutionEntryRepository programmingExerciseSolutionEntryRepository; + + @Autowired + protected ProgrammingExerciseStudentParticipationTestRepository programmingExerciseStudentParticipationRepository; + + @Autowired + protected ProgrammingExerciseTaskRepository taskRepository; + + @Autowired + protected ProgrammingExerciseTestCaseTestRepository testCaseRepository; + + @Autowired + protected ProgrammingExerciseTestRepository programmingExerciseRepository; + + @Autowired + protected ProgrammingSubmissionTestRepository programmingSubmissionRepository; + + @Autowired + protected SolutionProgrammingExerciseParticipationRepository solutionEntryRepository; + + @Autowired + protected StaticCodeAnalysisCategoryRepository staticCodeAnalysisCategoryRepository; + + @Autowired + protected TestwiseCoverageReportEntryRepository testwiseCoverageReportEntryRepository; + + @Autowired + protected UserIdeMappingRepository userIdeMappingRepository; + + // External Repositories + @Autowired + protected ComplaintRepository complaintRepo; + + @Autowired + protected ExamRepository examRepository; + + @Autowired + protected ExerciseTestRepository exerciseRepository; + + @Autowired + protected ParticipationTestRepository participationRepository; + + @Autowired + protected StudentParticipationTestRepository studentParticipationRepository; + + @Autowired + protected SubmissionTestRepository submissionRepository; + + @Autowired + protected UserTestRepository userRepository; + + // Services + @Autowired + protected AuxiliaryRepositoryService auxiliaryRepositoryService; + + @Autowired + protected BuildLogEntryService buildLogEntryService; + + @Autowired + protected CodeHintService codeHintService; + + @Autowired + protected ExerciseHintService exerciseHintService; + + @Autowired + protected GitUtilService gitUtilService; + + @Autowired + protected ProgrammingExerciseFeedbackCreationService feedbackCreationService; + + @Autowired + protected ProgrammingExerciseGradingService gradingService; + + @Autowired + protected ProgrammingExerciseRepositoryService programmingExerciseRepositoryService; + + @Autowired + protected ProgrammingExerciseTaskService programmingExerciseTaskService; + + // External Services + + // Util Services + @Autowired + protected ProgrammingExerciseIntegrationTestService programmingExerciseIntegrationTestService; + + @Autowired + protected ProgrammingExerciseUtilService programmingExerciseUtilService; + + // External Util Services + @Autowired + protected ComplaintUtilService complaintUtilService; + + @Autowired + protected CourseUtilService courseUtilService; + + @Autowired + protected ExamUtilService examUtilService; + + @Autowired + protected ExerciseUtilService exerciseUtilService; + + @Autowired + protected ParticipationUtilService participationUtilService; + + @Autowired + protected UserUtilService userUtilService; + +} diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationJenkinsGitlabTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationJenkinsGitlabTest.java new file mode 100644 index 000000000000..780343412cf9 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationJenkinsGitlabTest.java @@ -0,0 +1,209 @@ +package de.tum.cit.aet.artemis.programming; + +import java.net.URL; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.cit.aet.artemis.communication.repository.conversation.ChannelRepository; +import de.tum.cit.aet.artemis.communication.test_repository.PostTestRepository; +import de.tum.cit.aet.artemis.core.test_repository.UserTestRepository; +import de.tum.cit.aet.artemis.core.user.util.UserUtilService; +import de.tum.cit.aet.artemis.core.util.CourseTestService; +import de.tum.cit.aet.artemis.core.util.CourseUtilService; +import de.tum.cit.aet.artemis.exam.repository.ExamRepository; +import de.tum.cit.aet.artemis.exam.test_repository.StudentExamTestRepository; +import de.tum.cit.aet.artemis.exam.util.ExamUtilService; +import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; +import de.tum.cit.aet.artemis.exercise.test_repository.StudentParticipationTestRepository; +import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; +import de.tum.cit.aet.artemis.modeling.util.ModelingExerciseUtilService; +import de.tum.cit.aet.artemis.plagiarism.repository.PlagiarismCaseRepository; +import de.tum.cit.aet.artemis.plagiarism.repository.PlagiarismComparisonRepository; +import de.tum.cit.aet.artemis.programming.repository.BuildLogStatisticsEntryRepository; +import de.tum.cit.aet.artemis.programming.repository.BuildPlanRepository; +import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; +import de.tum.cit.aet.artemis.programming.service.BuildLogEntryService; +import de.tum.cit.aet.artemis.programming.service.ConsistencyCheckTestService; +import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseGradingService; +import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseImportService; +import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseService; +import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeatureService; +import de.tum.cit.aet.artemis.programming.service.RepositoryAccessService; +import de.tum.cit.aet.artemis.programming.service.gitlab.GitLabPersonalAccessTokenManagementService; +import de.tum.cit.aet.artemis.programming.service.jenkins.JenkinsAuthorizationInterceptor; +import de.tum.cit.aet.artemis.programming.service.jenkins.JenkinsInternalUrlService; +import de.tum.cit.aet.artemis.programming.service.jenkins.build_plan.JenkinsPipelineScriptCreator; +import de.tum.cit.aet.artemis.programming.service.jenkins.jobs.JenkinsJobPermissionsService; +import de.tum.cit.aet.artemis.programming.service.jenkins.jobs.JenkinsJobService; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseStudentParticipationTestRepository; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; +import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseResultTestService; +import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseTestService; +import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; +import de.tum.cit.aet.artemis.programming.util.ProgrammingSubmissionAndResultIntegrationTestService; +import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; +import de.tum.cit.aet.artemis.text.util.TextExerciseUtilService; + +public abstract class AbstractProgrammingIntegrationJenkinsGitlabTest extends AbstractSpringIntegrationJenkinsGitlabTest { + + // Config + @Value("${artemis.continuous-integration.artemis-authentication-token-value}") + protected String ARTEMIS_AUTHENTICATION_TOKEN_VALUE; + + @Value("${artemis.version-control.url}") + protected URL gitlabServerUrl; + + @Value("${artemis.git.name}") + protected String artemisGitName; + + @Value("${artemis.git.email}") + protected String artemisGitEmail; + + @Value("${artemis.continuous-integration.url}") + protected URL jenkinsServerUrl; + + @Autowired + protected RestTemplate restTemplate; + + @Autowired + protected ObjectMapper objectMapper; + + // Repositories + @Autowired + protected BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository; + + @Autowired + protected BuildPlanRepository buildPlanRepository; + + @Autowired + protected ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; + + @Autowired + protected ProgrammingExerciseStudentParticipationTestRepository participationRepository; + + @Autowired + protected ProgrammingExerciseTestCaseTestRepository testCaseRepository; + + @Autowired + protected ProgrammingExerciseTestRepository programmingExerciseRepository; + + @Autowired + protected ProgrammingSubmissionTestRepository submissionRepository; + + // External Repositories + @Autowired + protected ChannelRepository channelRepository; + + @Autowired + protected ExamRepository examRepository; + + @Autowired + protected PlagiarismCaseRepository plagiarismCaseRepository; + + @Autowired + protected PlagiarismComparisonRepository plagiarismComparisonRepository; + + @Autowired + protected PostTestRepository postRepository; + + @Autowired + protected StudentExamTestRepository studentExamRepository; + + @Autowired + protected StudentParticipationTestRepository studentParticipationRepository; + + @Autowired + protected UserTestRepository userRepository; + + // Services + @Autowired + protected BuildLogEntryService buildLogEntryService; + + @Autowired + protected ConsistencyCheckTestService consistencyCheckTestService; + + @Autowired + protected GitLabPersonalAccessTokenManagementService gitLabPersonalAccessTokenManagementService; + + @Autowired + protected JenkinsAuthorizationInterceptor jenkinsAuthorizationInterceptor; + + @Autowired + protected JenkinsInternalUrlService jenkinsInternalUrlService; + + @Autowired + protected JenkinsJobPermissionsService jenkinsJobPermissionsService; + + @Autowired + protected JenkinsJobService jenkinsJobService; + + @Autowired + protected JenkinsPipelineScriptCreator jenkinsPipelineScriptCreator; + + @Autowired + protected ProgrammingExerciseGradingService gradingService; + + @Autowired + protected ProgrammingExerciseImportService programmingExerciseImportService; + + @Autowired + protected ProgrammingExerciseIntegrationTestService programmingExerciseIntegrationTestService; + + @Autowired + protected ProgrammingExerciseService programmingExerciseService; + + @Autowired + protected ProgrammingLanguageFeatureService programmingLanguageFeatureService; + + @Autowired + protected ProgrammingSubmissionAndResultIntegrationTestService testService; + + @Autowired + protected RepositoryAccessService repositoryAccessService; + + // External Services + + // Util Services + @Autowired + protected ContinuousIntegrationTestService continuousIntegrationTestService; + + @Autowired + protected ProgrammingExerciseResultTestService programmingExerciseResultTestService; + + @Autowired + protected ProgrammingExerciseTestService programmingExerciseTestService; + + @Autowired + protected ProgrammingExerciseUtilService programmingExerciseUtilService; + + // External Util Services + @Autowired + protected CourseTestService courseTestService; + + @Autowired + protected CourseUtilService courseUtilService; + + @Autowired + protected ExamUtilService examUtilService; + + @Autowired + protected ExerciseUtilService exerciseUtilService; + + @Autowired + protected ModelingExerciseUtilService modelingExerciseUtilService; + + @Autowired + protected ParticipationUtilService participationUtilService; + + @Autowired + protected TextExerciseUtilService textExerciseUtilService; + + @Autowired + protected UserUtilService userUtilService; +} diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTest.java new file mode 100644 index 000000000000..fcbdc0bdef78 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTest.java @@ -0,0 +1,103 @@ +package de.tum.cit.aet.artemis.programming; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; + +import com.hazelcast.core.HazelcastInstance; + +import de.tum.cit.aet.artemis.atlas.competency.util.CompetencyUtilService; +import de.tum.cit.aet.artemis.buildagent.service.SharedQueueProcessingService; +import de.tum.cit.aet.artemis.core.connector.AeolusRequestMockProvider; +import de.tum.cit.aet.artemis.core.util.PageableSearchUtilService; +import de.tum.cit.aet.artemis.exam.util.ExamUtilService; +import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; +import de.tum.cit.aet.artemis.exercise.util.ExerciseIntegrationTestService; +import de.tum.cit.aet.artemis.programming.repository.StaticCodeAnalysisCategoryRepository; +import de.tum.cit.aet.artemis.programming.repository.VcsAccessLogRepository; +import de.tum.cit.aet.artemis.programming.service.BuildScriptProviderService; +import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseFeedbackCreationService; +import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseImportBasicService; +import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseTestCaseService; +import de.tum.cit.aet.artemis.programming.service.StaticCodeAnalysisService; +import de.tum.cit.aet.artemis.programming.service.aeolus.AeolusTemplateService; +import de.tum.cit.aet.artemis.programming.service.localci.SharedQueueManagementService; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; +import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; +import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; + +public abstract class AbstractProgrammingIntegrationLocalCILocalVCTest extends AbstractSpringIntegrationLocalCILocalVCTest { + + // Config + @Autowired + @Qualifier("hazelcastInstance") + protected HazelcastInstance hazelcastInstance; + + @Value("${artemis.user-management.internal-admin.username}") + protected String localVCUsername; + + @Value("${artemis.user-management.internal-admin.password}") + protected String localVCPassword; + + // Repositories + @Autowired + protected ProgrammingExerciseTestCaseTestRepository testCaseRepository; + + @Autowired + protected StaticCodeAnalysisCategoryRepository staticCodeAnalysisCategoryRepository; + + @Autowired + protected VcsAccessLogRepository vcsAccessLogRepository; + + // External Repositories + + // Services + @Autowired + protected AeolusRequestMockProvider aeolusRequestMockProvider; + + @Autowired + protected AeolusTemplateService aeolusTemplateService; + + @Autowired + protected BuildScriptProviderService buildScriptProviderService; + + @Autowired + protected ProgrammingExerciseFeedbackCreationService feedbackCreationService; + + @Autowired + protected ProgrammingExerciseImportBasicService programmingExerciseImportBasicService; + + @Autowired + protected ProgrammingExerciseTestCaseService testCaseService; + + @Autowired + protected SharedQueueManagementService sharedQueueManagementService; + + @Autowired + protected SharedQueueProcessingService sharedQueueProcessingService; + + @Autowired + protected StaticCodeAnalysisService staticCodeAnalysisService; + + // External Services + + // Util Services + @Autowired + protected ProgrammingExerciseUtilService programmingExerciseUtilService; + + // External Util Services + @Autowired + protected CompetencyUtilService competencyUtilService; + + @Autowired + protected ExamUtilService examUtilService; + + @Autowired + protected ExerciseIntegrationTestService exerciseIntegrationTestService; + + @Autowired + protected PageableSearchUtilService pageableSearchUtilService; + + @Autowired + protected ParticipationUtilService participationUtilService; +} diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/AbstractLocalCILocalVCIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTestBase.java similarity index 63% rename from src/test/java/de/tum/cit/aet/artemis/programming/icl/AbstractLocalCILocalVCIntegrationTest.java rename to src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTestBase.java index 0f559f0c3a50..56a168f7e776 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/AbstractLocalCILocalVCIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTestBase.java @@ -1,9 +1,10 @@ -package de.tum.cit.aet.artemis.programming.icl; +package de.tum.cit.aet.artemis.programming; import java.time.ZonedDateTime; import java.util.List; import java.util.Set; +import org.apache.sshd.server.SshServer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; @@ -18,23 +19,76 @@ import de.tum.cit.aet.artemis.core.user.util.UserUtilService; import de.tum.cit.aet.artemis.exam.repository.ExamRepository; import de.tum.cit.aet.artemis.exam.test_repository.StudentExamTestRepository; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; import de.tum.cit.aet.artemis.exercise.repository.TeamRepository; +import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProjectType; import de.tum.cit.aet.artemis.programming.domain.SolutionProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.TemplateProgrammingExerciseParticipation; +import de.tum.cit.aet.artemis.programming.hestia.util.HestiaUtilTestService; import de.tum.cit.aet.artemis.programming.repository.AuxiliaryRepositoryRepository; -import de.tum.cit.aet.artemis.programming.service.StaticCodeAnalysisService; -import de.tum.cit.aet.artemis.programming.service.aeolus.AeolusTemplateService; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; +import de.tum.cit.aet.artemis.programming.repository.SolutionProgrammingExerciseParticipationRepository; +import de.tum.cit.aet.artemis.programming.repository.hestia.CoverageFileReportRepository; +import de.tum.cit.aet.artemis.programming.repository.hestia.CoverageReportRepository; +import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseGitDiffReportRepository; +import de.tum.cit.aet.artemis.programming.repository.hestia.TestwiseCoverageReportEntryRepository; +import de.tum.cit.aet.artemis.programming.service.BuildLogEntryService; +import de.tum.cit.aet.artemis.programming.service.ParticipationVcsAccessTokenService; +import de.tum.cit.aet.artemis.programming.service.hestia.ProgrammingExerciseGitDiffReportService; +import de.tum.cit.aet.artemis.programming.service.hestia.TestwiseCoverageService; +import de.tum.cit.aet.artemis.programming.service.hestia.behavioral.BehavioralTestCaseService; +import de.tum.cit.aet.artemis.programming.service.hestia.structural.StructuralTestCaseService; +import de.tum.cit.aet.artemis.programming.service.localci.LocalCIResultService; +import de.tum.cit.aet.artemis.programming.service.localci.LocalCITriggerService; +import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCServletService; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; +import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; + +/** + * This adds upon the {@link AbstractProgrammingIntegrationLocalCILocalVCTest} by providing additional + *
    + *
  • test data on test startup
  • + *
  • services and repositories that are needed for the tests.
  • + *
+ */ +public abstract class AbstractProgrammingIntegrationLocalCILocalVCTestBase extends AbstractProgrammingIntegrationLocalCILocalVCTest { + + // Config + @Value("${artemis.version-control.user}") + protected String localVCBaseUsername; -public abstract class AbstractLocalCILocalVCIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { + @LocalServerPort + protected int port; @Autowired - protected TeamRepository teamRepository; + protected SshServer sshServer; + + // Repositories + @Autowired + protected AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; + + @Autowired + protected CoverageFileReportRepository coverageFileReportRepository; + + @Autowired + protected CoverageReportRepository coverageReportRepository; + + @Autowired + protected ProgrammingExerciseGitDiffReportRepository reportRepository; + + @Autowired + protected ProgrammingExerciseTestRepository programmingExerciseRepository; + + @Autowired + protected ProgrammingSubmissionTestRepository programmingSubmissionRepository; + + @Autowired + protected SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseRepository; + + @Autowired + protected TestwiseCoverageReportEntryRepository testwiseCoverageReportEntryRepository; + // External Repositories @Autowired protected ExamRepository examRepository; @@ -42,28 +96,49 @@ public abstract class AbstractLocalCILocalVCIntegrationTest extends AbstractSpri protected StudentExamTestRepository studentExamRepository; @Autowired - protected UserUtilService userUtilService; + protected TeamRepository teamRepository; + // Services @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; + protected BehavioralTestCaseService behavioralTestCaseService; @Autowired - protected ParticipationUtilService participationUtilService; + protected BuildLogEntryService buildLogEntryService; @Autowired - private StaticCodeAnalysisService staticCodeAnalysisService; + protected HestiaUtilTestService hestiaUtilTestService; @Autowired - protected AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; + protected LocalCIResultService localCIResultService; @Autowired - private AeolusTemplateService aeolusTemplateService; + protected LocalVCServletService localVCServletService; - @Value("${artemis.version-control.user}") - protected String localVCBaseUsername; + @Autowired + protected LocalCITriggerService localCITriggerService; - @LocalServerPort - protected int port; + @Autowired + protected ParticipationVcsAccessTokenService participationVcsAccessTokenService; + + @Autowired + protected ProgrammingExerciseGitDiffReportService reportService; + + @Autowired + protected StructuralTestCaseService structuralTestCaseService; + + @Autowired + protected TestwiseCoverageService testwiseCoverageService; + + // External Services + + // Util Services + + // External Util services + @Autowired + protected ExerciseUtilService exerciseUtilService; + + @Autowired + protected UserUtilService userUtilService; // The error messages returned by JGit contain these Strings that correspond to the HTTP status codes. protected static final String NOT_FOUND = "not found"; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/AuxiliaryRepositoryServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/AuxiliaryRepositoryServiceTest.java index 6bb1268ff527..7108644c2fe3 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/AuxiliaryRepositoryServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/AuxiliaryRepositoryServiceTest.java @@ -8,39 +8,17 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; import de.tum.cit.aet.artemis.programming.domain.AuxiliaryRepository; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; -import de.tum.cit.aet.artemis.programming.repository.AuxiliaryRepositoryRepository; -import de.tum.cit.aet.artemis.programming.service.AuxiliaryRepositoryService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class AuxiliaryRepositoryServiceTest extends AbstractSpringIntegrationIndependentTest { +class AuxiliaryRepositoryServiceTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_INVALID_LENGTH_STRING = "a".repeat(AuxiliaryRepository.MAX_NAME_LENGTH + 1); private static final String TEST_INVALID_DESCRIPTION_LENGTH_STRING = "a".repeat(AuxiliaryRepository.MAX_DESCRIPTION_LENGTH + 1); - @Autowired - private AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; - - @Autowired - private AuxiliaryRepositoryService auxiliaryRepositoryService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - private static ProgrammingExercise programmingExerciseBeforeUpdate; private static ProgrammingExercise updatedProgrammingExercise; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/BuildPlanIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/BuildPlanIntegrationTest.java index adab0997cd23..82809c5875e2 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/BuildPlanIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/BuildPlanIntegrationTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -14,28 +13,11 @@ import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; import de.tum.cit.aet.artemis.programming.domain.ProjectType; import de.tum.cit.aet.artemis.programming.domain.build.BuildPlan; -import de.tum.cit.aet.artemis.programming.repository.BuildPlanRepository; -import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class BuildPlanIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class BuildPlanIntegrationTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "buildplanintegration"; - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; - - @Autowired - private BuildPlanRepository buildPlanRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - private ProgrammingExercise programmingExercise; @BeforeEach diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ConsistencyCheckGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ConsistencyCheckGitlabJenkinsIntegrationTest.java index 38b1447bc645..5f11ce522fc4 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ConsistencyCheckGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ConsistencyCheckGitlabJenkinsIntegrationTest.java @@ -3,16 +3,9 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.cit.aet.artemis.programming.service.ConsistencyCheckTestService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; - -class ConsistencyCheckGitlabJenkinsIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { - - @Autowired - private ConsistencyCheckTestService consistencyCheckTestService; +class ConsistencyCheckGitlabJenkinsIntegrationTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { @BeforeEach void setup() throws Exception { diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java index f23887c80d43..ffe1a7bf34a6 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java @@ -13,37 +13,18 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MvcResult; -import com.fasterxml.jackson.databind.ObjectMapper; - import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.util.CourseFactory; -import de.tum.cit.aet.artemis.core.util.CourseTestService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class CourseGitlabJenkinsIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class CourseGitlabJenkinsIntegrationTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "courseegitlabjenkins"; - @Autowired - private CourseTestService courseTestService; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - @BeforeEach void setup() { courseTestService.setup(TEST_PREFIX, this); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/GitServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/GitServiceTest.java index f03dc054e698..123d6cf796d3 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/GitServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/GitServiceTest.java @@ -31,7 +31,6 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; import de.tum.cit.aet.artemis.core.exception.GitException; import de.tum.cit.aet.artemis.core.user.util.UserFactory; @@ -39,12 +38,8 @@ import de.tum.cit.aet.artemis.programming.domain.FileType; import de.tum.cit.aet.artemis.programming.domain.Repository; import de.tum.cit.aet.artemis.programming.util.GitUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class GitServiceTest extends AbstractSpringIntegrationIndependentTest { - - @Autowired - private GitUtilService gitUtilService; +class GitServiceTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "gitservice"; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/GitlabServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/GitlabServiceTest.java index 8e857fed08c2..6d220e121508 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/GitlabServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/GitlabServiceTest.java @@ -8,7 +8,6 @@ import static org.mockito.Mockito.verify; import java.net.URISyntaxException; -import java.net.URL; import java.util.Optional; import java.util.stream.Stream; @@ -20,8 +19,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -33,24 +30,8 @@ import de.tum.cit.aet.artemis.programming.domain.Commit; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; -import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class GitlabServiceTest extends AbstractSpringIntegrationJenkinsGitlabTest { - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Value("${artemis.version-control.url}") - private URL gitlabServerUrl; +class GitlabServiceTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { @BeforeEach void initTestCase() { diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/IdePreferencesIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/IdePreferencesIntegrationTest.java index 888424193c1e..acec92af2f2b 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/IdePreferencesIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/IdePreferencesIntegrationTest.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; @@ -19,20 +18,11 @@ import de.tum.cit.aet.artemis.programming.domain.ide.UserIdeMapping; import de.tum.cit.aet.artemis.programming.dto.IdeDTO; import de.tum.cit.aet.artemis.programming.dto.IdeMappingDTO; -import de.tum.cit.aet.artemis.programming.repository.settings.IdeRepository; -import de.tum.cit.aet.artemis.programming.repository.settings.UserIdeMappingRepository; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class IdePreferencesIntegrationTest extends AbstractSpringIntegrationIndependentTest { +class IdePreferencesIntegrationTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "idepreferencesintegration"; - @Autowired - private UserIdeMappingRepository userIdeMappingRepository; - - @Autowired - private IdeRepository ideRepository; - private Ide VsCode; private Ide IntelliJ; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/PlantUmlIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/PlantUmlIntegrationTest.java index 304ab40f1351..58e167265d15 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/PlantUmlIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/PlantUmlIntegrationTest.java @@ -17,11 +17,10 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; import net.sourceforge.plantuml.SourceStringReader; import net.sourceforge.plantuml.core.DiagramDescription; -class PlantUmlIntegrationTest extends AbstractSpringIntegrationIndependentTest { +class PlantUmlIntegrationTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "plantumlintegration"; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingAssessmentIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingAssessmentIntegrationTest.java index b39e0c0bc704..137bbab18fb0 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingAssessmentIntegrationTest.java @@ -20,7 +20,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; @@ -34,16 +33,12 @@ import de.tum.cit.aet.artemis.assessment.domain.LongFeedbackText; import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.assessment.dto.AssessmentUpdateDTO; -import de.tum.cit.aet.artemis.assessment.repository.ComplaintRepository; -import de.tum.cit.aet.artemis.assessment.util.ComplaintUtilService; import de.tum.cit.aet.artemis.core.config.Constants; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.util.TestResourceUtils; import de.tum.cit.aet.artemis.exam.domain.Exam; import de.tum.cit.aet.artemis.exam.domain.ExerciseGroup; -import de.tum.cit.aet.artemis.exam.repository.ExamRepository; -import de.tum.cit.aet.artemis.exam.util.ExamUtilService; import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.exercise.domain.IncludedInOverallScore; import de.tum.cit.aet.artemis.exercise.domain.InitializationState; @@ -51,21 +46,13 @@ import de.tum.cit.aet.artemis.exercise.domain.SubmissionType; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationFactory; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; -import de.tum.cit.aet.artemis.exercise.test_repository.StudentParticipationTestRepository; -import de.tum.cit.aet.artemis.exercise.test_repository.SubmissionTestRepository; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; import de.tum.cit.aet.artemis.programming.dto.ResultDTO; -import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class ProgrammingAssessmentIntegrationTest extends AbstractSpringIntegrationIndependentTest { +class ProgrammingAssessmentIntegrationTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "programmingassessment"; @@ -73,39 +60,6 @@ class ProgrammingAssessmentIntegrationTest extends AbstractSpringIntegrationInde private final Double offsetByTenThousandth = 0.0001; - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; - - @Autowired - private ComplaintRepository complaintRepo; - - @Autowired - private ProgrammingSubmissionTestRepository programmingSubmissionRepository; - - @Autowired - private ExamRepository examRepository; - - @Autowired - private StudentParticipationTestRepository studentParticipationRepository; - - @Autowired - private SubmissionTestRepository submissionRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - - @Autowired - private ExamUtilService examUtilService; - - @Autowired - private ComplaintUtilService complaintUtilService; - private ProgrammingExercise programmingExercise; private ProgrammingSubmission programmingSubmission; @@ -1011,7 +965,7 @@ void overrideProgrammingAssessmentAfterComplaint() throws Exception { void unlockFeedbackRequestAfterAssessment() throws Exception { programmingExercise.setAllowFeedbackRequests(true); programmingExercise.setDueDate(ZonedDateTime.now().plusDays(1)); - exerciseRepository.save(programmingExercise); + programmingExerciseRepository.save(programmingExercise); var participation = programmingExerciseStudentParticipation; participation.setIndividualDueDate(ZonedDateTime.now().minusDays(1)); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseBuildPlanTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseBuildPlanTest.java index 074fe70e54b9..0bde4cd2fa51 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseBuildPlanTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseBuildPlanTest.java @@ -4,23 +4,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.util.LinkedMultiValueMap; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationGitlabCIGitlabSamlTest; -class ProgrammingExerciseBuildPlanTest extends AbstractSpringIntegrationGitlabCIGitlabSamlTest { - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; +class ProgrammingExerciseBuildPlanTest extends AbstractProgrammingIntegrationGitlabCIGitlabSamlTest { private static final String BUILD_PLAN = """ image: ubuntu:20.04 @@ -32,9 +21,6 @@ class ProgrammingExerciseBuildPlanTest extends AbstractSpringIntegrationGitlabCI - echo "Test" """; - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - private Long programmingExerciseId; @BeforeEach diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGitIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGitIntegrationTest.java index 0fb13f48c51a..5882a3517915 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGitIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGitIntegrationTest.java @@ -22,39 +22,21 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; import de.tum.cit.aet.artemis.programming.service.GitService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.GitUtilService; import de.tum.cit.aet.artemis.programming.util.LocalRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class ProgrammingExerciseGitIntegrationTest extends AbstractSpringIntegrationIndependentTest { +class ProgrammingExerciseGitIntegrationTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "progexgitintegration"; private static final String COMBINE_COMMITS_ENDPOINT = "/api/programming-exercises/{exerciseId}/combine-template-commits"; - @Autowired - private GitUtilService gitUtilService; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - private File localRepoFile; private Git localGit; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGitlabJenkinsIntegrationTest.java index 8918d1190bef..53ff9474ac5c 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGitlabJenkinsIntegrationTest.java @@ -32,7 +32,6 @@ import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.MockedStatic; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -44,20 +43,11 @@ import de.tum.cit.aet.artemis.exercise.domain.SubmissionType; import de.tum.cit.aet.artemis.programming.domain.AeolusTarget; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; -import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeatureService; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseTestService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class ProgrammingExerciseGitlabJenkinsIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class ProgrammingExerciseGitlabJenkinsIntegrationTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "progexgitlabjenkins"; - @Autowired - private ProgrammingExerciseTestService programmingExerciseTestService; - - @Autowired - private ProgrammingLanguageFeatureService programmingLanguageFeatureService; - @BeforeEach void setup() throws Exception { programmingExerciseTestService.setupTestUsers(TEST_PREFIX, 0, 0, 0, 0); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGradingServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGradingServiceTest.java index b38a7248ae5f..2993b02caccb 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGradingServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGradingServiceTest.java @@ -23,7 +23,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.test.context.TestSecurityContextHolder; @@ -36,19 +35,12 @@ import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.assessment.domain.Visibility; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; -import de.tum.cit.aet.artemis.core.util.CourseUtilService; import de.tum.cit.aet.artemis.core.util.RoundingUtil; import de.tum.cit.aet.artemis.exam.domain.Exam; import de.tum.cit.aet.artemis.exam.domain.ExerciseGroup; -import de.tum.cit.aet.artemis.exam.repository.ExamRepository; -import de.tum.cit.aet.artemis.exam.util.ExamUtilService; import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.exercise.domain.participation.Participation; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; -import de.tum.cit.aet.artemis.exercise.test_repository.StudentParticipationTestRepository; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; @@ -56,14 +48,8 @@ import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; import de.tum.cit.aet.artemis.programming.domain.SolutionProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.dto.ProgrammingExerciseGradingStatisticsDTO; -import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; -import de.tum.cit.aet.artemis.programming.repository.StaticCodeAnalysisCategoryRepository; import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseGradingService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; /** * Tests the {@link ProgrammingExerciseGradingService}. @@ -76,49 +62,10 @@ *
  • {@link ExamProgrammingExerciseGradingServiceTest} - for exercises in an exam setting.
  • * */ -abstract class ProgrammingExerciseGradingServiceTest extends AbstractSpringIntegrationIndependentTest { +abstract class ProgrammingExerciseGradingServiceTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "progexgradingservice"; - @Autowired - private ProgrammingExerciseTestCaseTestRepository testCaseRepository; - - @Autowired - private StudentParticipationTestRepository studentParticipationRepository; - - @Autowired - private StaticCodeAnalysisCategoryRepository staticCodeAnalysisCategoryRepository; - - @Autowired - private ExamRepository examRepository; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; - - @Autowired - private ProgrammingExerciseGradingService gradingService; - - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - - @Autowired - private CourseUtilService courseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - - @Autowired - private ExamUtilService examUtilService; - private ProgrammingExercise programmingExerciseSCAEnabled; private ProgrammingExercise programmingExercise; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationJenkinsGitlabTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationJenkinsGitlabTest.java index 6906106f8fac..5812c2923e0e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationJenkinsGitlabTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationJenkinsGitlabTest.java @@ -15,26 +15,17 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; -import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseService; import de.tum.cit.aet.artemis.programming.util.ArgumentSources; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class ProgrammingExerciseIntegrationJenkinsGitlabTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class ProgrammingExerciseIntegrationJenkinsGitlabTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "progexjenkgitlab"; - @Autowired - private ProgrammingExerciseIntegrationTestService programmingExerciseIntegrationTestService; - - @Autowired - private ProgrammingExerciseService programmingExerciseService; - @BeforeEach void initTestCase() throws Exception { gitlabRequestMockProvider.enableMockingOfRequests(); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationTestService.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationTestService.java index 49696af61d96..23739bdaacd3 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseIntegrationTestService.java @@ -128,7 +128,7 @@ * 1) Jenkins + Gitlab */ @Service -class ProgrammingExerciseIntegrationTestService { +public class ProgrammingExerciseIntegrationTestService { private static final String NON_EXISTING_ID = Integer.toString(Integer.MAX_VALUE); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseParticipationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseParticipationIntegrationTest.java index 1e47b62e649b..843cb7e0f378 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseParticipationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseParticipationIntegrationTest.java @@ -25,7 +25,6 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; @@ -37,9 +36,6 @@ import de.tum.cit.aet.artemis.exercise.domain.Submission; import de.tum.cit.aet.artemis.exercise.domain.participation.Participation; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; -import de.tum.cit.aet.artemis.exercise.test_repository.ParticipationTestRepository; -import de.tum.cit.aet.artemis.exercise.test_repository.StudentParticipationTestRepository; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; @@ -49,13 +45,8 @@ import de.tum.cit.aet.artemis.programming.domain.TemplateProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; import de.tum.cit.aet.artemis.programming.dto.CommitInfoDTO; -import de.tum.cit.aet.artemis.programming.repository.AuxiliaryRepositoryRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseStudentParticipationTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class ProgrammingExerciseParticipationIntegrationTest extends AbstractSpringIntegrationIndependentTest { +class ProgrammingExerciseParticipationIntegrationTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "programmingexerciseparticipation"; @@ -63,31 +54,6 @@ class ProgrammingExerciseParticipationIntegrationTest extends AbstractSpringInte private final String exercisesBaseUrl = "/api/programming-exercises/"; - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private StudentParticipationTestRepository studentParticipationRepository; - - @Autowired - private ParticipationTestRepository participationRepository; - - @Autowired - private ProgrammingExerciseStudentParticipationTestRepository programmingExerciseStudentParticipationRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - - @Autowired - private AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; - - // TODO remove again after refactoring and cleanup - @Autowired - private ProgrammingExerciseIntegrationTestService programmingExerciseIntegrationTestService; - private ProgrammingExercise programmingExercise; private Participation programmingExerciseParticipation; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseRepositoryServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseRepositoryServiceTest.java index c045fe08c76b..1a7f91039e94 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseRepositoryServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseRepositoryServiceTest.java @@ -10,40 +10,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; -import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseRepositoryService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class ProgrammingExerciseRepositoryServiceTest extends AbstractSpringIntegrationIndependentTest { +class ProgrammingExerciseRepositoryServiceTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "progexreposervice"; - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseRepositoryService programmingExerciseRepositoryService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - - @Autowired - private UserUtilService userUtilService; - private ProgrammingExercise programmingExerciseBeforeUpdate; private ProgrammingExercise updatedProgrammingExercise; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseResultJenkinsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseResultJenkinsIntegrationTest.java index 7308c6a6dae5..9e0650403412 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseResultJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseResultJenkinsIntegrationTest.java @@ -16,7 +16,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.mockito.ArgumentMatchers; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.config.Constants; @@ -25,16 +24,11 @@ import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; import de.tum.cit.aet.artemis.programming.service.ci.notification.dto.CommitDTO; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseResultTestService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class ProgrammingExerciseResultJenkinsIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class ProgrammingExerciseResultJenkinsIntegrationTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "progexresultjenk"; - @Autowired - private ProgrammingExerciseResultTestService programmingExerciseResultTestService; - @BeforeEach void setup() { programmingExerciseResultTestService.setup(TEST_PREFIX); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseScheduleServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseScheduleServiceTest.java index 3fd82c87864c..b386fb22d033 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseScheduleServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseScheduleServiceTest.java @@ -25,73 +25,27 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InOrder; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.assessment.domain.AssessmentType; import de.tum.cit.aet.artemis.assessment.domain.Visibility; import de.tum.cit.aet.artemis.core.config.Constants; import de.tum.cit.aet.artemis.core.domain.User; -import de.tum.cit.aet.artemis.core.service.messaging.InstanceMessageReceiveService; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; import de.tum.cit.aet.artemis.exam.domain.Exam; import de.tum.cit.aet.artemis.exam.domain.StudentExam; -import de.tum.cit.aet.artemis.exam.repository.ExamRepository; -import de.tum.cit.aet.artemis.exam.test_repository.StudentExamTestRepository; -import de.tum.cit.aet.artemis.exam.util.ExamUtilService; import de.tum.cit.aet.artemis.exercise.domain.ExerciseLifecycle; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; import de.tum.cit.aet.artemis.programming.domain.ParticipationLifecycle; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseStudentParticipationTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.util.LocalRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationGitlabCIGitlabSamlTest; -class ProgrammingExerciseScheduleServiceTest extends AbstractSpringIntegrationGitlabCIGitlabSamlTest { +class ProgrammingExerciseScheduleServiceTest extends AbstractProgrammingIntegrationGitlabCIGitlabSamlTest { private static final String TEST_PREFIX = "programmingexercisescheduleservice"; - @Autowired - private InstanceMessageReceiveService instanceMessageReceiveService; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseStudentParticipationTestRepository participationRepository; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository programmingExerciseTestCaseRepository; - - @Autowired - private ExamRepository examRepository; - - @Autowired - private StudentExamTestRepository studentExamRepository; - - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - - @Autowired - private ExamUtilService examUtilService; - private ProgrammingExercise programmingExercise; private final LocalRepository studentRepository = new LocalRepository(defaultBranch); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseServiceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseServiceIntegrationTest.java index 4ab18f1ce22a..e32956ac6bd8 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseServiceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseServiceIntegrationTest.java @@ -12,13 +12,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.core.util.PageableSearchUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseIntegrationTestService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.StaticCodeAnalysisCategory; @@ -27,33 +24,14 @@ import de.tum.cit.aet.artemis.programming.domain.submissionpolicy.LockRepositoryPolicy; import de.tum.cit.aet.artemis.programming.domain.submissionpolicy.SubmissionPenaltyPolicy; import de.tum.cit.aet.artemis.programming.domain.submissionpolicy.SubmissionPolicy; -import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseImportBasicService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; -class ProgrammingExerciseServiceIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { +class ProgrammingExerciseServiceIntegrationTest extends AbstractProgrammingIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "progexserviceintegration"; private static final String BASE_RESOURCE = "/api/programming-exercises"; - @Autowired - ProgrammingExerciseImportBasicService programmingExerciseImportBasicService; - - @Autowired - ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ExerciseIntegrationTestService exerciseIntegrationTestService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private PageableSearchUtilService pageableSearchUtilService; - private Course additionalEmptyCourse; private ProgrammingExercise programmingExercise; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseServiceTest.java index 028ac2405428..353461ecf77a 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseServiceTest.java @@ -7,35 +7,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class ProgrammingExerciseServiceTest extends AbstractSpringIntegrationIndependentTest { +class ProgrammingExerciseServiceTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "progexservice"; - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseTestRepository; - private ProgrammingExercise programmingExercise1; private ProgrammingExercise programmingExercise2; @@ -63,7 +42,7 @@ void shouldFindProgrammingExerciseWithBuildAndTestDateInFuture() { programmingExercise2.setBuildAndTestStudentSubmissionsAfterDueDate(ZonedDateTime.now().minusHours(1)); programmingExerciseRepository.save(programmingExercise2); - List programmingExercises = programmingExerciseTestRepository.findAllWithBuildAndTestAfterDueDateInFuture(); + List programmingExercises = programmingExerciseRepository.findAllWithBuildAndTestAfterDueDateInFuture(); assertThat(programmingExercises).contains(programmingExercise1).doesNotContain(programmingExercise2); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTemplateIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTemplateIntegrationTest.java index 618437fd78b9..909814b1e829 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTemplateIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTemplateIntegrationTest.java @@ -47,7 +47,6 @@ import org.junit.jupiter.params.provider.MethodSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -55,14 +54,11 @@ import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; import de.tum.cit.aet.artemis.programming.domain.ProjectType; -import de.tum.cit.aet.artemis.programming.service.ProgrammingLanguageFeatureService; import de.tum.cit.aet.artemis.programming.util.LocalRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseTestService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ProgrammingExerciseTemplateIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class ProgrammingExerciseTemplateIntegrationTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final Logger log = LoggerFactory.getLogger(ProgrammingExerciseTemplateIntegrationTest.class); @@ -70,12 +66,6 @@ class ProgrammingExerciseTemplateIntegrationTest extends AbstractSpringIntegrati private static File java17Home; - @Autowired - private ProgrammingExerciseTestService programmingExerciseTestService; - - @Autowired - private ProgrammingLanguageFeatureService programmingLanguageFeatureService; - private ProgrammingExercise exercise; private final LocalRepository exerciseRepo = new LocalRepository(defaultBranch); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTest.java index 587478746a89..0679190ab7b9 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTest.java @@ -18,64 +18,30 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.assessment.domain.AssessmentType; import de.tum.cit.aet.artemis.communication.domain.conversation.Channel; -import de.tum.cit.aet.artemis.communication.repository.conversation.ChannelRepository; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; import de.tum.cit.aet.artemis.exam.domain.StudentExam; -import de.tum.cit.aet.artemis.exam.util.ExamUtilService; import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.exercise.domain.InitializationState; import de.tum.cit.aet.artemis.exercise.domain.Submission; import de.tum.cit.aet.artemis.exercise.domain.SubmissionType; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseStudentParticipationTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class ProgrammingExerciseTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class ProgrammingExerciseTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "peinttest"; - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository programmingExerciseTestCaseRepository; - - @Autowired - private ProgrammingExerciseStudentParticipationTestRepository participationRepository; - - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - - @Autowired - private ExamUtilService examUtilService; - private Long programmingExerciseId; - @Autowired - private ChannelRepository channelRepository; - @BeforeEach void init() { userUtilService.addUsers(TEST_PREFIX, 2, 2, 0, 2); @@ -170,7 +136,7 @@ void updateExerciseAutomaticFeedbackNoTestCases() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseRepository .findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndBuildConfigById(programmingExerciseId).orElseThrow(); - Set testCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExercise.getId()); + Set testCases = testCaseRepository.findByExerciseId(programmingExercise.getId()); assertThat(testCases).isEmpty(); // no test cases, changing to automatic feedback: update should work @@ -198,9 +164,9 @@ void updateExerciseTestCasesZeroWeight(AssessmentType assessmentType) throws Exc .findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesAndBuildConfigById(programmingExerciseId).orElseThrow(); programmingExerciseUtilService.addTestCasesToProgrammingExercise(programmingExercise); - Set testCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExercise.getId()); + Set testCases = testCaseRepository.findByExerciseId(programmingExercise.getId()); testCases.forEach(testCase -> testCase.setWeight(0D)); - programmingExerciseTestCaseRepository.saveAll(testCases); + testCaseRepository.saveAll(testCases); programmingExercise.setAssessmentType(assessmentType); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTestCaseServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTestCaseServiceTest.java index cf293bf0740c..504d895a9200 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTestCaseServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTestCaseServiceTest.java @@ -17,55 +17,21 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.assessment.domain.AssessmentType; import de.tum.cit.aet.artemis.assessment.domain.Visibility; import de.tum.cit.aet.artemis.core.security.SecurityUtils; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; import de.tum.cit.aet.artemis.exam.domain.ExerciseGroup; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.dto.ProgrammingExerciseTestCaseDTO; -import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseFeedbackCreationService; -import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseTestCaseService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; -class ProgrammingExerciseTestCaseServiceTest extends AbstractSpringIntegrationLocalCILocalVCTest { +class ProgrammingExerciseTestCaseServiceTest extends AbstractProgrammingIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "progextestcase"; - @Autowired - private ProgrammingExerciseTestCaseTestRepository testCaseRepository; - - @Autowired - private ProgrammingExerciseTestCaseService testCaseService; - - @Autowired - private ProgrammingExerciseFeedbackCreationService feedbackCreationService; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - private ProgrammingExercise programmingExercise; @BeforeEach diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingSubmissionAndResultGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingSubmissionAndResultGitlabJenkinsIntegrationTest.java index 354a3e9b65c5..102643f76237 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingSubmissionAndResultGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingSubmissionAndResultGitlabJenkinsIntegrationTest.java @@ -16,8 +16,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -29,55 +27,23 @@ import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.security.SecurityUtils; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; import de.tum.cit.aet.artemis.programming.domain.ProjectType; import de.tum.cit.aet.artemis.programming.domain.build.BuildLogEntry; -import de.tum.cit.aet.artemis.programming.repository.BuildLogStatisticsEntryRepository; -import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.cit.aet.artemis.programming.service.ci.notification.dto.CommitDTO; import de.tum.cit.aet.artemis.programming.service.ci.notification.dto.TestCaseDTO; import de.tum.cit.aet.artemis.programming.service.ci.notification.dto.TestCaseDetailMessageDTO; import de.tum.cit.aet.artemis.programming.service.ci.notification.dto.TestResultsDTO; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.programming.util.ProgrammingSubmissionAndResultIntegrationTestService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class ProgrammingSubmissionAndResultGitlabJenkinsIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class ProgrammingSubmissionAndResultGitlabJenkinsIntegrationTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "progsubresgitlabjen"; - @Value("${artemis.continuous-integration.artemis-authentication-token-value}") - private String ARTEMIS_AUTHENTICATION_TOKEN_VALUE; - - @Autowired - private ProgrammingSubmissionTestRepository submissionRepository; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; - - @Autowired - private BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - private ProgrammingExercise exercise; - @Autowired - private ProgrammingSubmissionAndResultIntegrationTestService testService; - @BeforeEach void setUp() { jenkinsRequestMockProvider.enableMockingOfRequests(jenkinsServer); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingSubmissionIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingSubmissionIntegrationTest.java index 4d53882cd461..6e6ba01f1c5c 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingSubmissionIntegrationTest.java @@ -27,8 +27,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -50,53 +48,18 @@ import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.exercise.dto.SubmissionDTO; import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationFactory; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; -import de.tum.cit.aet.artemis.exercise.test_repository.StudentParticipationTestRepository; import de.tum.cit.aet.artemis.modeling.domain.ModelingExercise; import de.tum.cit.aet.artemis.modeling.domain.ModelingSubmission; -import de.tum.cit.aet.artemis.modeling.util.ModelingExerciseUtilService; import de.tum.cit.aet.artemis.programming.domain.Commit; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseStudentParticipationTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class ProgrammingSubmissionIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class ProgrammingSubmissionIntegrationTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "programmingsubmission"; - @Value("${artemis.git.name}") - private String artemisGitName; - - @Value("${artemis.git.email}") - private String artemisGitEmail; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingSubmissionTestRepository submissionRepository; - - @Autowired - private ProgrammingExerciseStudentParticipationTestRepository programmingExerciseStudentParticipationRepository; - - @Autowired - private StudentParticipationTestRepository studentParticipationRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - - @Autowired - private ModelingExerciseUtilService modelingExerciseUtilService; - private ProgrammingExercise exercise; private ProgrammingExerciseStudentParticipation programmingExerciseStudentParticipation; @@ -394,7 +357,7 @@ void triggerFailedBuildResultPresentInCIOk() throws Exception { submission.setCommitHash(TestConstants.COMMIT_HASH_STRING); submission.setType(SubmissionType.MANUAL); submission = programmingExerciseUtilService.addProgrammingSubmission(exercise, submission, TEST_PREFIX + "student1"); - var optionalParticipation = programmingExerciseStudentParticipationRepository.findById(submission.getParticipation().getId()); + var optionalParticipation = participationRepository.findById(submission.getParticipation().getId()); assertThat(optionalParticipation).isPresent(); final var participation = optionalParticipation.get(); jenkinsRequestMockProvider.enableMockingOfRequests(jenkinsServer); @@ -446,11 +409,11 @@ private ProgrammingExerciseStudentParticipation createExerciseWithSubmissionAndP var submission = new ProgrammingSubmission(); submission.setType(SubmissionType.MANUAL); submission = programmingExerciseUtilService.addProgrammingSubmission(exercise, submission, user.getLogin()); - var optionalParticipation = programmingExerciseStudentParticipationRepository.findById(submission.getParticipation().getId()); + var optionalParticipation = participationRepository.findById(submission.getParticipation().getId()); assertThat(optionalParticipation).isPresent(); var participation = optionalParticipation.get(); participation.setBuildPlanId(null); - participation = programmingExerciseStudentParticipationRepository.save(participation); + participation = participationRepository.save(participation); return participation; } @@ -842,7 +805,7 @@ void testGetProgrammingSubmissionWithoutAssessmentWithIndividualDueDate(boolean else { submission.getParticipation().setIndividualDueDate(ZonedDateTime.now().minusDays(1)); } - programmingExerciseStudentParticipationRepository.save((ProgrammingExerciseStudentParticipation) submission.getParticipation()); + participationRepository.save((ProgrammingExerciseStudentParticipation) submission.getParticipation()); participationUtilService.addResultToSubmission(submission, AssessmentType.AUTOMATIC, null); String url = "/api/exercises/" + exercise.getId() + "/programming-submission-without-assessment"; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/RepositoryIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/RepositoryIntegrationTest.java index cc602c83f30d..64307de24282 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/RepositoryIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/RepositoryIntegrationTest.java @@ -43,7 +43,6 @@ import org.mockito.MockedStatic; import org.mockito.stubbing.Answer; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; @@ -54,25 +53,17 @@ import ch.qos.logback.core.read.ListAppender; import de.tum.cit.aet.artemis.assessment.domain.AssessmentType; import de.tum.cit.aet.artemis.communication.domain.Post; -import de.tum.cit.aet.artemis.communication.test_repository.PostTestRepository; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.util.TestConstants; import de.tum.cit.aet.artemis.exam.domain.Exam; -import de.tum.cit.aet.artemis.exam.repository.ExamRepository; -import de.tum.cit.aet.artemis.exam.test_repository.StudentExamTestRepository; -import de.tum.cit.aet.artemis.exam.util.ExamUtilService; import de.tum.cit.aet.artemis.exercise.domain.InitializationState; import de.tum.cit.aet.artemis.exercise.domain.SubmissionType; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; -import de.tum.cit.aet.artemis.exercise.test_repository.StudentParticipationTestRepository; import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismCase; import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismComparison; import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismStatus; import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismSubmission; import de.tum.cit.aet.artemis.plagiarism.domain.text.TextSubmissionElement; -import de.tum.cit.aet.artemis.plagiarism.repository.PlagiarismCaseRepository; -import de.tum.cit.aet.artemis.plagiarism.repository.PlagiarismComparisonRepository; import de.tum.cit.aet.artemis.programming.domain.FileType; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; @@ -81,20 +72,14 @@ import de.tum.cit.aet.artemis.programming.domain.build.BuildLogEntry; import de.tum.cit.aet.artemis.programming.dto.FileMove; import de.tum.cit.aet.artemis.programming.dto.RepositoryStatusDTO; -import de.tum.cit.aet.artemis.programming.service.BuildLogEntryService; import de.tum.cit.aet.artemis.programming.service.GitService; import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseParticipationService; import de.tum.cit.aet.artemis.programming.service.vcs.VersionControlRepositoryPermission; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseStudentParticipationTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.util.GitUtilService; import de.tum.cit.aet.artemis.programming.util.LocalRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; import de.tum.cit.aet.artemis.programming.web.repository.FileSubmission; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -import de.tum.cit.aet.artemis.text.util.TextExerciseUtilService; -class RepositoryIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class RepositoryIntegrationTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "repositoryintegration"; @@ -102,45 +87,6 @@ class RepositoryIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTe private final String filesContentBaseUrl = "/api/repository-files-content/"; - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private StudentParticipationTestRepository studentParticipationRepository; - - @Autowired - private ExamRepository examRepository; - - @Autowired - private StudentExamTestRepository studentExamRepository; - - @Autowired - private PlagiarismComparisonRepository plagiarismComparisonRepository; - - @Autowired - private PlagiarismCaseRepository plagiarismCaseRepository; - - @Autowired - private PostTestRepository postRepository; - - @Autowired - private BuildLogEntryService buildLogEntryService; - - @Autowired - private ProgrammingExerciseStudentParticipationTestRepository programmingExerciseStudentParticipationRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - - @Autowired - private TextExerciseUtilService textExerciseUtilService; - - @Autowired - private ExamUtilService examUtilService; - private ProgrammingExercise programmingExercise; private final String currentLocalFileName = "currentFileName"; @@ -1081,7 +1027,7 @@ void testCommitChangesNotAllowedForLockedParticipation() throws Exception { programmingExercise.setReleaseDate(ZonedDateTime.now().minusHours(2)); programmingExercise.setDueDate(ZonedDateTime.now().minusHours(1)); programmingExerciseRepository.save(programmingExercise); - this.programmingExerciseStudentParticipationRepository.updateLockedById(participation.getId(), true); + participationRepository.updateLockedById(participation.getId(), true); // Committing is not allowed var receivedStatusBeforeCommit = request.get(studentRepoBaseUrl + participation.getId(), HttpStatus.OK, RepositoryStatusDTO.class); @@ -1104,7 +1050,7 @@ void testResetNotAllowedForLockedParticipation() throws Exception { programmingExercise.setReleaseDate(ZonedDateTime.now().minusHours(2)); programmingExercise.setDueDate(ZonedDateTime.now().minusHours(1)); programmingExerciseRepository.save(programmingExercise); - this.programmingExerciseStudentParticipationRepository.updateLockedById(participation.getId(), true); + participationRepository.updateLockedById(participation.getId(), true); assertUnchangedRepositoryStatusForForbiddenReset(); } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/RepositoryProgrammingExerciseParticipationJenkinsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/RepositoryProgrammingExerciseParticipationJenkinsIntegrationTest.java index 0616cda099c4..325c4520311b 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/RepositoryProgrammingExerciseParticipationJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/RepositoryProgrammingExerciseParticipationJenkinsIntegrationTest.java @@ -13,7 +13,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -22,27 +21,14 @@ import de.tum.cit.aet.artemis.core.util.TestConstants; import de.tum.cit.aet.artemis.exercise.domain.SubmissionType; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; import de.tum.cit.aet.artemis.programming.domain.build.BuildLogEntry; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class RepositoryProgrammingExerciseParticipationJenkinsIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class RepositoryProgrammingExerciseParticipationJenkinsIntegrationTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "repoprogexpartjenk"; - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - @BeforeEach void setup() throws Exception { userUtilService.addUsers(TEST_PREFIX, 1, 1, 0, 1); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/StaticCodeAnalysisIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/StaticCodeAnalysisIntegrationTest.java index 06f673f808ee..2a6cccd7d15e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/StaticCodeAnalysisIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/StaticCodeAnalysisIntegrationTest.java @@ -15,7 +15,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -34,33 +33,12 @@ import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; import de.tum.cit.aet.artemis.programming.domain.StaticCodeAnalysisCategory; import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisIssue; -import de.tum.cit.aet.artemis.programming.repository.StaticCodeAnalysisCategoryRepository; -import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseFeedbackCreationService; -import de.tum.cit.aet.artemis.programming.service.StaticCodeAnalysisService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; -class StaticCodeAnalysisIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { +class StaticCodeAnalysisIntegrationTest extends AbstractProgrammingIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "staticcodeanalysis"; - @Autowired - private StaticCodeAnalysisService staticCodeAnalysisService; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private StaticCodeAnalysisCategoryRepository staticCodeAnalysisCategoryRepository; - - @Autowired - private ProgrammingExerciseFeedbackCreationService feedbackCreationService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - private ProgrammingExercise programmingExerciseSCAEnabled; private ProgrammingExercise programmingExercise; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/SubmissionPolicyIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/SubmissionPolicyIntegrationTest.java index 660eec38c4f6..6fe76f858ff9 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/SubmissionPolicyIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/SubmissionPolicyIntegrationTest.java @@ -14,7 +14,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -22,7 +21,6 @@ import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.exercise.domain.Submission; import de.tum.cit.aet.artemis.exercise.domain.SubmissionType; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; @@ -30,29 +28,13 @@ import de.tum.cit.aet.artemis.programming.domain.submissionpolicy.LockRepositoryPolicy; import de.tum.cit.aet.artemis.programming.domain.submissionpolicy.SubmissionPenaltyPolicy; import de.tum.cit.aet.artemis.programming.domain.submissionpolicy.SubmissionPolicy; -import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseGradingService; import de.tum.cit.aet.artemis.programming.service.ci.notification.dto.CommitDTO; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class SubmissionPolicyIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class SubmissionPolicyIntegrationTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "submissionpolicyintegration"; - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseGradingService gradingService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - private Long programmingExerciseId; private ProgrammingExercise programmingExercise; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/TestRepositoryResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/TestRepositoryResourceIntegrationTest.java index dfaf90733fac..1a43719cc32b 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/TestRepositoryResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/TestRepositoryResourceIntegrationTest.java @@ -25,7 +25,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; @@ -38,25 +37,16 @@ import de.tum.cit.aet.artemis.programming.dto.FileMove; import de.tum.cit.aet.artemis.programming.dto.RepositoryStatusDTO; import de.tum.cit.aet.artemis.programming.dto.RepositoryStatusDTOType; -import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.cit.aet.artemis.programming.service.GitService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.util.GitUtilService; import de.tum.cit.aet.artemis.programming.util.LocalRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; import de.tum.cit.aet.artemis.programming.web.repository.FileSubmission; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class TestRepositoryResourceIntegrationTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class TestRepositoryResourceIntegrationTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "testrepositoryresourceint"; - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; - private final String testRepoBaseUrl = "/api/test-repository/"; private ProgrammingExercise programmingExercise; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/architecture/ProgrammingTestArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/architecture/ProgrammingTestArchitectureTest.java new file mode 100644 index 000000000000..b434af7971bd --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/programming/architecture/ProgrammingTestArchitectureTest.java @@ -0,0 +1,31 @@ +package de.tum.cit.aet.artemis.programming.architecture; + +import java.util.Set; + +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationGitlabCIGitlabSamlTest; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationIndependentTest; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationJenkinsGitlabTest; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTest; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase; +import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleTestArchitectureTest; + +class ProgrammingTestArchitectureTest extends AbstractModuleTestArchitectureTest { + + @Override + public String getModulePackage() { + return ARTEMIS_PACKAGE + ".programming"; + } + + @Override + protected Set> getAbstractModuleIntegrationTestClasses() { + // @formatter:off + return Set.of( + AbstractProgrammingIntegrationGitlabCIGitlabSamlTest.class, + AbstractProgrammingIntegrationIndependentTest.class, + AbstractProgrammingIntegrationJenkinsGitlabTest.class, + AbstractProgrammingIntegrationLocalCILocalVCTest.class, + AbstractProgrammingIntegrationLocalCILocalVCTestBase.class + ); + // @formatter:on + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/CodeHintIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/CodeHintIntegrationTest.java index a4d326fe43a8..a49fe18c881c 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/CodeHintIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/CodeHintIntegrationTest.java @@ -9,37 +9,20 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationIndependentTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.hestia.CodeHint; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseSolutionEntry; -import de.tum.cit.aet.artemis.programming.repository.hestia.CodeHintRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseSolutionEntryRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class CodeHintIntegrationTest extends AbstractSpringIntegrationIndependentTest { +class CodeHintIntegrationTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "codehint"; - @Autowired - private CodeHintRepository codeHintRepository; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository testCaseRepository; - - @Autowired - private ProgrammingExerciseSolutionEntryRepository solutionEntryRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - private ProgrammingExercise exercise; private CodeHint codeHint; @@ -143,7 +126,7 @@ void updateSolutionEntriesOnSaving() throws Exception { newEntry.setPreviousCode("New previous code"); var testCase = testCases.get("test1"); newEntry.setTestCase(testCase); - var savedNewEntry = solutionEntryRepository.save(newEntry); + var savedNewEntry = programmingExerciseSolutionEntryRepository.save(newEntry); savedNewEntry.setTestCase(testCase); codeHint.setSolutionEntries(new HashSet<>(Set.of(changedEntry, savedNewEntry))); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/CodeHintServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/CodeHintServiceTest.java index 634e86de81b9..05a72ac9a78d 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/CodeHintServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/CodeHintServiceTest.java @@ -12,57 +12,24 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.assessment.domain.Visibility; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationIndependentTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.hestia.CodeHint; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseSolutionEntry; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTask; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTestCaseType; -import de.tum.cit.aet.artemis.programming.repository.hestia.CodeHintRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseSolutionEntryRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository; -import de.tum.cit.aet.artemis.programming.service.hestia.CodeHintService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; @SuppressWarnings("ArraysAsListWithZeroOrOneArgument") -class CodeHintServiceTest extends AbstractSpringIntegrationIndependentTest { +class CodeHintServiceTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "codehintservice"; - @Autowired - private CodeHintService codeHintService; - - @Autowired - private CodeHintRepository codeHintRepository; - - @Autowired - private ProgrammingExerciseTaskRepository taskRepository; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository testCaseRepository; - - @Autowired - private ProgrammingExerciseSolutionEntryRepository solutionEntryRepository; - - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - private ProgrammingExercise exercise; @BeforeEach @@ -88,7 +55,7 @@ private ProgrammingExerciseSolutionEntry addSolutionEntryToTestCase(ProgrammingE solutionEntry.setTestCase(testCase); solutionEntry.setLine(1); solutionEntry.setCode("code"); - return solutionEntryRepository.save(solutionEntry); + return programmingExerciseSolutionEntryRepository.save(solutionEntry); } private ProgrammingExerciseTask addTaskToExercise(String name, List testCases) { @@ -114,7 +81,7 @@ private CodeHint addCodeHintToTask(String name, ProgrammingExerciseTask task, Se solutionEntries.forEach(entry -> entry.setCodeHint(codeHint)); var createdHint = codeHintRepository.save(codeHint); - solutionEntryRepository.saveAll(solutionEntries); + programmingExerciseSolutionEntryRepository.saveAll(solutionEntries); return createdHint; } @@ -195,7 +162,7 @@ void testUpdateTestCaseOfSolutionEntry() { entryToUpdate.setTestCase(testCase2); codeHintService.updateSolutionEntriesForCodeHint(codeHint); - var allEntries = solutionEntryRepository.findByExerciseIdWithTestCases(exercise.getId()); + var allEntries = programmingExerciseSolutionEntryRepository.findByExerciseIdWithTestCases(exercise.getId()); assertThat(allEntries).hasSize(1); assertThat(allEntries.stream().findAny().orElseThrow().getTestCase().getId()).isEqualTo(testCase2.getId()); } @@ -216,7 +183,7 @@ void testUpdatedContentOfSolutionEntry() { entry.setFilePath("Updated file path"); codeHintService.updateSolutionEntriesForCodeHint(codeHint); - var allEntries = solutionEntryRepository.findByExerciseIdWithTestCases(exercise.getId()); + var allEntries = programmingExerciseSolutionEntryRepository.findByExerciseIdWithTestCases(exercise.getId()); assertThat(allEntries).hasSize(1); assertThat(allEntries.stream().findAny().orElseThrow()).isEqualTo(entryToUpdate); } @@ -233,7 +200,7 @@ void testSaveWithNewSolutionEntry() { codeHint.setSolutionEntries(new HashSet<>(Set.of(manuallyCreatedEntry))); codeHintService.updateSolutionEntriesForCodeHint(codeHint); - var allEntries = solutionEntryRepository.findByExerciseIdWithTestCases(exercise.getId()); + var allEntries = programmingExerciseSolutionEntryRepository.findByExerciseIdWithTestCases(exercise.getId()); assertThat(allEntries).containsExactly(manuallyCreatedEntry); } @@ -249,10 +216,10 @@ void testSaveWithRemovedSolutionEntry() { codeHint.setSolutionEntries(new HashSet<>(Collections.emptySet())); codeHintService.updateSolutionEntriesForCodeHint(codeHint); - var entriesForHint = solutionEntryRepository.findByCodeHintId(codeHint.getId()); + var entriesForHint = programmingExerciseSolutionEntryRepository.findByCodeHintId(codeHint.getId()); assertThat(entriesForHint).isEmpty(); - var allEntries = solutionEntryRepository.findByExerciseIdWithTestCases(exercise.getId()); + var allEntries = programmingExerciseSolutionEntryRepository.findByExerciseIdWithTestCases(exercise.getId()); assertThat(allEntries).containsExactly(entryToRemove); } @@ -273,10 +240,10 @@ void testSaveEntryWithTestCaseUnrelatedToHintTask() { codeHint.setSolutionEntries(new HashSet<>(Set.of(invalidSolutionEntry))); assertThatExceptionOfType(BadRequestAlertException.class).isThrownBy(() -> codeHintService.updateSolutionEntriesForCodeHint(codeHint)); - var entriesForHint = solutionEntryRepository.findByCodeHintId(codeHint.getId()); + var entriesForHint = programmingExerciseSolutionEntryRepository.findByCodeHintId(codeHint.getId()); assertThat(entriesForHint).isEmpty(); - var allEntries = solutionEntryRepository.findByExerciseIdWithTestCases(exercise.getId()); + var allEntries = programmingExerciseSolutionEntryRepository.findByExerciseIdWithTestCases(exercise.getId()); assertThat(allEntries).isEmpty(); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ExerciseHintIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ExerciseHintIntegrationTest.java index adaa6e600832..6a44f54820fe 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ExerciseHintIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ExerciseHintIntegrationTest.java @@ -11,7 +11,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -21,7 +20,7 @@ import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.assessment.domain.Visibility; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationIndependentTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; @@ -29,43 +28,11 @@ import de.tum.cit.aet.artemis.programming.domain.hestia.ExerciseHint; import de.tum.cit.aet.artemis.programming.domain.hestia.ExerciseHintActivation; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTask; -import de.tum.cit.aet.artemis.programming.repository.hestia.ExerciseHintActivationRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ExerciseHintRepository; -import de.tum.cit.aet.artemis.programming.service.hestia.ProgrammingExerciseTaskService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class ExerciseHintIntegrationTest extends AbstractSpringIntegrationIndependentTest { +class ExerciseHintIntegrationTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "exercisehintintegration"; - @Autowired - private ExerciseHintRepository exerciseHintRepository; - - @Autowired - private ProgrammingExerciseTestRepository exerciseRepository; - - @Autowired - private ProgrammingExerciseTaskService programmingExerciseTaskService; - - @Autowired - private ProgrammingSubmissionTestRepository programmingSubmissionRepository; - - @Autowired - private ExerciseHintActivationRepository exerciseHintActivationRepository; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository programmingExerciseTestCaseRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - private ProgrammingExercise exercise; private ProgrammingExercise exerciseLite; @@ -85,9 +52,8 @@ void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 2, 2, 1, 2); - programmingExerciseTestCaseRepository - .saveAll(programmingExerciseTestCaseRepository.findByExerciseId(programmingExercise.getId()).stream().peek(testCase -> testCase.setActive(true)).toList()); - exerciseLite = exerciseRepository.findByIdElseThrow(programmingExercise.getId()); + testCaseRepository.saveAll(testCaseRepository.findByExerciseId(programmingExercise.getId()).stream().peek(testCase -> testCase.setActive(true)).toList()); + exerciseLite = programmingExerciseRepository.findByIdElseThrow(programmingExercise.getId()); exercise = programmingExerciseUtilService.loadProgrammingExerciseWithEagerReferences(exerciseLite); programmingExerciseUtilService.addHintsToExercise(exercise); programmingExerciseUtilService.addTasksToProgrammingExercise(exercise); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ExerciseHintServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ExerciseHintServiceTest.java index 69279ac37859..99b3d357f73f 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ExerciseHintServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ExerciseHintServiceTest.java @@ -10,7 +10,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import de.tum.cit.aet.artemis.assessment.domain.AssessmentType; import de.tum.cit.aet.artemis.assessment.domain.Feedback; @@ -19,69 +18,17 @@ import de.tum.cit.aet.artemis.assessment.domain.Visibility; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; -import de.tum.cit.aet.artemis.core.test_repository.UserTestRepository; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationIndependentTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.hestia.ExerciseHint; import de.tum.cit.aet.artemis.programming.domain.hestia.ExerciseHintActivation; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTask; -import de.tum.cit.aet.artemis.programming.repository.hestia.ExerciseHintActivationRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ExerciseHintRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository; -import de.tum.cit.aet.artemis.programming.service.hestia.ExerciseHintService; -import de.tum.cit.aet.artemis.programming.service.hestia.ProgrammingExerciseTaskService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; - -class ExerciseHintServiceTest extends AbstractSpringIntegrationIndependentTest { - private static final String TEST_PREFIX = "exercisehintservice"; - - @Autowired - private UserTestRepository userRepository; - - @Autowired - private ExerciseHintService exerciseHintService; - - @Autowired - private ExerciseHintRepository exerciseHintRepository; - - @Autowired - private ProgrammingExerciseTestRepository exerciseRepository; - - @Autowired - private ProgrammingExerciseTaskService programmingExerciseTaskService; - - @Autowired - private ProgrammingSubmissionTestRepository programmingSubmissionRepository; +class ExerciseHintServiceTest extends AbstractProgrammingIntegrationIndependentTest { - @Autowired - private ExerciseHintActivationRepository exerciseHintActivationRepository; - - @Autowired - private ProgrammingExerciseTaskRepository programmingExerciseTaskRepository; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository programmingExerciseTestCaseRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; + private static final String TEST_PREFIX = "exercisehintservice"; private ProgrammingExercise exercise; @@ -107,9 +54,9 @@ void initTestCase() { student = userRepository.getUserWithGroupsAndAuthorities(TEST_PREFIX + "student1"); userUtilService.changeUser(TEST_PREFIX + "student1"); - var activatedTestCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExercise.getId()).stream().peek(testCase -> testCase.setActive(true)).toList(); - programmingExerciseTestCaseRepository.saveAll(activatedTestCases); - exercise = exerciseRepository.findByIdElseThrow(programmingExercise.getId()); + var activatedTestCases = testCaseRepository.findByExerciseId(programmingExercise.getId()).stream().peek(testCase -> testCase.setActive(true)).toList(); + testCaseRepository.saveAll(activatedTestCases); + exercise = programmingExerciseRepository.findByIdElseThrow(programmingExercise.getId()); exercise = programmingExerciseUtilService.loadProgrammingExerciseWithEagerReferences(exercise); programmingExerciseUtilService.addHintsToExercise(exercise); programmingExerciseUtilService.addTasksToProgrammingExercise(exercise); @@ -133,10 +80,10 @@ void testGetAvailableExerciseHintsTasksWithoutTestCases() { addResultWithFailedTestCases(exercise.getTestCases()); for (ProgrammingExerciseTask sortedTask : sortedTasks) { sortedTask.getTestCases().clear(); - programmingExerciseTaskRepository.save(sortedTask); + taskRepository.save(sortedTask); } exercise.setProblemStatement(exercise.getProblemStatement().replaceAll("\\([^()]+\\)", "()")); - exerciseRepository.save(exercise); + programmingExerciseRepository.save(exercise); var availableExerciseHints = exerciseHintService.getAvailableExerciseHints(exercise, student); assertThat(availableExerciseHints).isEmpty(); } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/HestiaDatabaseTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/HestiaDatabaseTest.java index 2267bf5acaae..040547ae9cf9 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/HestiaDatabaseTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/HestiaDatabaseTest.java @@ -8,57 +8,24 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationIndependentTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.hestia.CodeHint; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseSolutionEntry; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTask; -import de.tum.cit.aet.artemis.programming.repository.hestia.CodeHintRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseSolutionEntryRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; /** * This class tests the database relations of the Hestia domain models. * This currently includes ProgrammingExerciseTask, ProgrammingExerciseSolutionEntry and CodeHint. * It tests if the addition and deletion of these models works as expected. */ -class HestiaDatabaseTest extends AbstractSpringIntegrationIndependentTest { +class HestiaDatabaseTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "hestiadatabase"; - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository programmingExerciseTestCaseRepository; - - @Autowired - private ProgrammingExerciseTaskRepository programmingExerciseTaskRepository; - - @Autowired - private ProgrammingExerciseSolutionEntryRepository programmingExerciseSolutionEntryRepository; - - @Autowired - private CodeHintRepository codeHintRepository; - - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - private Long programmingExerciseId; @BeforeEach @@ -72,7 +39,7 @@ ProgrammingExerciseTask addTaskToProgrammingExercise(String taskName) { var task = new ProgrammingExerciseTask(); task.setTaskName(taskName); task.setExercise(programmingExerciseRepository.getReferenceById(programmingExerciseId)); - task = programmingExerciseTaskRepository.save(task); + task = taskRepository.save(task); return task; } @@ -92,21 +59,21 @@ ProgrammingExerciseSolutionEntry[] addSolutionEntriesToTestCase(int count, Progr @Test void addOneTaskToProgrammingExercise() { var task = addTaskToProgrammingExercise("Task 1"); - assertThat(programmingExerciseTaskRepository.findByExerciseIdWithTestCases(programmingExerciseId)).containsExactly(task); + assertThat(taskRepository.findByExerciseIdWithTestCases(programmingExerciseId)).containsExactly(task); } @Test void deleteProgrammingExerciseWithTask() { addOneTaskToProgrammingExercise(); programmingExerciseRepository.deleteById(programmingExerciseId); - assertThat(programmingExerciseTaskRepository.findByExerciseId(programmingExerciseId)).isEmpty(); + assertThat(taskRepository.findByExerciseId(programmingExerciseId)).isEmpty(); } @Test void addTestCasesWithSolutionEntriesToProgrammingExercise() { var programmingExercise = programmingExerciseRepository.findByIdElseThrow(programmingExerciseId); programmingExerciseUtilService.addTestCasesToProgrammingExercise(programmingExercise); - var testCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExerciseId); + var testCases = testCaseRepository.findByExerciseId(programmingExerciseId); assertThat(testCases).isNotEmpty(); for (ProgrammingExerciseTestCase testCase : testCases) { var solutionEntries = addSolutionEntriesToTestCase(2, testCase); @@ -118,7 +85,7 @@ void addTestCasesWithSolutionEntriesToProgrammingExercise() { void deleteProgrammingExerciseWithTestCasesAndSolutionEntries() { addTestCasesWithSolutionEntriesToProgrammingExercise(); programmingExerciseRepository.deleteById(programmingExerciseId); - assertThat(programmingExerciseTestCaseRepository.findByExerciseId(programmingExerciseId)).isEmpty(); + assertThat(testCaseRepository.findByExerciseId(programmingExerciseId)).isEmpty(); assertThat(programmingExerciseSolutionEntryRepository.findByExerciseIdWithTestCases(programmingExerciseId)).isEmpty(); } @@ -126,24 +93,24 @@ void deleteProgrammingExerciseWithTestCasesAndSolutionEntries() { void deleteTaskWithTestCases() { var programmingExercise = programmingExerciseRepository.findByIdElseThrow(programmingExerciseId); programmingExerciseUtilService.addTestCasesToProgrammingExercise(programmingExercise); - var testCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExerciseId); + var testCases = testCaseRepository.findByExerciseId(programmingExerciseId); assertThat(testCases).isNotEmpty(); var task = addTaskToProgrammingExercise("Task 1"); task.setTestCases(testCases); - task = programmingExerciseTaskRepository.save(task); - programmingExerciseTaskRepository.delete(task); - assertThat(programmingExerciseTestCaseRepository.findByExerciseId(programmingExerciseId)).isEqualTo(testCases); + task = taskRepository.save(task); + taskRepository.delete(task); + assertThat(testCaseRepository.findByExerciseId(programmingExerciseId)).isEqualTo(testCases); } @Test void addCodeHintToProgrammingExercise() { var programmingExercise = programmingExerciseRepository.findByIdElseThrow(programmingExerciseId); programmingExerciseUtilService.addTestCasesToProgrammingExercise(programmingExercise); - var testCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExerciseId); + var testCases = testCaseRepository.findByExerciseId(programmingExerciseId); assertThat(testCases).isNotEmpty(); var task = addTaskToProgrammingExercise("Task 1"); task.setTestCases(testCases); - task = programmingExerciseTaskRepository.save(task); + task = taskRepository.save(task); Set allSolutionEntries = new HashSet<>(); for (ProgrammingExerciseTestCase testCase : testCases) { var solutionEntries = addSolutionEntriesToTestCase(2, testCase); @@ -162,7 +129,7 @@ void addCodeHintToProgrammingExercise() { codeHint.setSolutionEntries(allSolutionEntries); codeHint = codeHintRepository.save(codeHint); task.setExerciseHints(Set.of(codeHint)); - programmingExerciseTaskRepository.save(task); + taskRepository.save(task); assertThat(programmingExerciseSolutionEntryRepository.findByCodeHintId(codeHint.getId())).isEqualTo(allSolutionEntries); assertThat(codeHintRepository.findByExerciseId(programmingExerciseId)).containsExactly(codeHint); } @@ -172,7 +139,7 @@ void deleteCodeHint() { addCodeHintToProgrammingExercise(); var codeHint = codeHintRepository.findByExerciseId(programmingExerciseId).stream().findAny().orElseThrow(); codeHintRepository.delete(codeHint); - assertThat(programmingExerciseTaskRepository.findByExerciseId(programmingExerciseId)).hasSize(1); + assertThat(taskRepository.findByExerciseId(programmingExerciseId)).hasSize(1); assertThat(programmingExerciseSolutionEntryRepository.findByExerciseIdWithTestCases(programmingExerciseId)).hasSize(6); } @@ -180,19 +147,19 @@ void deleteCodeHint() { void deleteProgrammingExerciseWithCodeHint() { addCodeHintToProgrammingExercise(); programmingExerciseRepository.deleteById(programmingExerciseId); - assertThat(programmingExerciseTaskRepository.findByExerciseId(programmingExerciseId)).isEmpty(); + assertThat(taskRepository.findByExerciseId(programmingExerciseId)).isEmpty(); assertThat(programmingExerciseSolutionEntryRepository.findByExerciseIdWithTestCases(programmingExerciseId)).isEmpty(); assertThat(codeHintRepository.findByExerciseId(programmingExerciseId)).isEmpty(); - assertThat(programmingExerciseTestCaseRepository.findByExerciseId(programmingExerciseId)).isEmpty(); + assertThat(testCaseRepository.findByExerciseId(programmingExerciseId)).isEmpty(); } @Test void deleteTaskWithCodeHint() { addCodeHintToProgrammingExercise(); - var task = programmingExerciseTaskRepository.findByExerciseId(programmingExerciseId).stream().findAny().orElseThrow(); - programmingExerciseTaskRepository.delete(task); + var task = taskRepository.findByExerciseId(programmingExerciseId).stream().findAny().orElseThrow(); + taskRepository.delete(task); assertThat(codeHintRepository.findByExerciseId(programmingExerciseId)).isEmpty(); - assertThat(programmingExerciseTestCaseRepository.findByExerciseId(programmingExerciseId)).hasSize(3); + assertThat(testCaseRepository.findByExerciseId(programmingExerciseId)).hasSize(3); assertThat(programmingExerciseSolutionEntryRepository.findByExerciseIdWithTestCases(programmingExerciseId)).hasSize(6); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java index 6d5bf267139a..df54e4dc10f5 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java @@ -7,24 +7,21 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseGitDiffEntry; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseGitDiffReport; -import de.tum.cit.aet.artemis.programming.hestia.util.HestiaUtilTestService; -import de.tum.cit.aet.artemis.programming.icl.AbstractLocalCILocalVCIntegrationTest; -import de.tum.cit.aet.artemis.programming.service.hestia.ProgrammingExerciseGitDiffReportService; import de.tum.cit.aet.artemis.programming.util.LocalRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; /** * Tests for the ProgrammingExerciseGitDiffReportResource */ -class ProgrammingExerciseGitDiffReportIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { +class ProgrammingExerciseGitDiffReportIntegrationTest extends AbstractProgrammingIntegrationLocalCILocalVCTestBase { private static final String TEST_PREFIX = "progexgitdiffreport"; @@ -40,12 +37,6 @@ class ProgrammingExerciseGitDiffReportIntegrationTest extends AbstractLocalCILoc private ProgrammingExercise exercise; - @Autowired - private HestiaUtilTestService hestiaUtilTestService; - - @Autowired - private ProgrammingExerciseGitDiffReportService reportService; - @BeforeEach void initTestCase() throws Exception { Course course = courseUtilService.addEmptyCourse(); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportServiceTest.java index 70e3c6fb8301..2516a2f416fc 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportServiceTest.java @@ -9,38 +9,22 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseGitDiffEntry; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseGitDiffReport; -import de.tum.cit.aet.artemis.programming.hestia.util.HestiaUtilTestService; -import de.tum.cit.aet.artemis.programming.icl.AbstractLocalCILocalVCIntegrationTest; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseGitDiffReportRepository; -import de.tum.cit.aet.artemis.programming.service.hestia.ProgrammingExerciseGitDiffReportService; import de.tum.cit.aet.artemis.programming.util.LocalRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; /** * Tests for the ProgrammingExerciseGitDiffReportService */ -class ProgrammingExerciseGitDiffReportServiceTest extends AbstractLocalCILocalVCIntegrationTest { +class ProgrammingExerciseGitDiffReportServiceTest extends AbstractProgrammingIntegrationLocalCILocalVCTestBase { private static final String TEST_PREFIX = "progexgitdiffreportservice"; - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - private static final String FILE_NAME = "test.java"; private final LocalRepository solutionRepo = new LocalRepository("main"); @@ -49,15 +33,6 @@ class ProgrammingExerciseGitDiffReportServiceTest extends AbstractLocalCILocalVC private ProgrammingExercise exercise; - @Autowired - private HestiaUtilTestService hestiaUtilTestService; - - @Autowired - private ProgrammingExerciseGitDiffReportService reportService; - - @Autowired - private ProgrammingExerciseGitDiffReportRepository reportRepository; - @Override protected String getTestPrefix() { return TEST_PREFIX; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseSolutionEntryIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseSolutionEntryIntegrationTest.java index 4bf515d2e4db..17d326cfc221 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseSolutionEntryIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseSolutionEntryIntegrationTest.java @@ -8,41 +8,20 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationIndependentTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.hestia.CodeHint; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseSolutionEntry; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTask; -import de.tum.cit.aet.artemis.programming.repository.hestia.CodeHintRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseSolutionEntryRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class ProgrammingExerciseSolutionEntryIntegrationTest extends AbstractSpringIntegrationIndependentTest { +class ProgrammingExerciseSolutionEntryIntegrationTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "progexsolutionentry"; - @Autowired - private ProgrammingExerciseSolutionEntryRepository programmingExerciseSolutionEntryRepository; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository programmingExerciseTestCaseRepository; - - @Autowired - private ProgrammingExerciseTaskRepository programmingExerciseTaskRepository; - - @Autowired - private CodeHintRepository codeHintRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - private ProgrammingExercise programmingExercise; private CodeHint codeHint; @@ -53,7 +32,7 @@ void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 2, 2, 1, 2); programmingExercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); - Set testCases = programmingExerciseTestCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()); + Set testCases = testCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()); codeHint = new CodeHint(); codeHint.setExercise(programmingExercise); @@ -76,7 +55,7 @@ void initTestCase() { task.setExercise(programmingExercise); task.setTaskName("Task"); task.setTestCases(new HashSet<>(testCases)); - task = programmingExerciseTaskRepository.save(task); + task = taskRepository.save(task); codeHint.setProgrammingExerciseTask(task); codeHintRepository.save(codeHint); } @@ -119,8 +98,7 @@ void testGetSolutionEntriesByCodeHintId() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testGetSolutionEntriesByTestCaseId() throws Exception { - ProgrammingExerciseTestCase testCase = programmingExerciseTestCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst() - .orElseThrow(); + ProgrammingExerciseTestCase testCase = testCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst().orElseThrow(); final var solutionEntries = new HashSet<>( request.getList("/api/programming-exercises/" + programmingExercise.getId() + "/test-cases/" + testCase.getId() + "/solution-entries", HttpStatus.OK, ProgrammingExerciseSolutionEntry.class)); @@ -139,8 +117,7 @@ void testGetAllSolutionEntries() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") void testDeleteSolutionEntry() throws Exception { - ProgrammingExerciseTestCase testCase = programmingExerciseTestCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst() - .orElseThrow(); + ProgrammingExerciseTestCase testCase = testCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst().orElseThrow(); Long entryId = testCase.getSolutionEntries().stream().findFirst().orElseThrow().getId(); request.delete("/api/programming-exercises/" + programmingExercise.getId() + "/test-cases/" + testCase.getId() + "/solution-entries/" + entryId, HttpStatus.NO_CONTENT); assertThat(programmingExerciseSolutionEntryRepository.findById(entryId)).isEmpty(); @@ -149,8 +126,7 @@ void testDeleteSolutionEntry() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "STUDENT") void testDeleteSolutionEntryAsStudent() throws Exception { - ProgrammingExerciseTestCase testCase = programmingExerciseTestCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst() - .orElseThrow(); + ProgrammingExerciseTestCase testCase = testCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst().orElseThrow(); Long entryId = testCase.getSolutionEntries().stream().findFirst().orElseThrow().getId(); request.delete("/api/programming-exercises/" + programmingExercise.getId() + "/test-cases/" + testCase.getId() + "/solution-entries/" + entryId, HttpStatus.FORBIDDEN); } @@ -158,8 +134,7 @@ void testDeleteSolutionEntryAsStudent() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testDeleteSolutionEntryAsTutor() throws Exception { - ProgrammingExerciseTestCase testCase = programmingExerciseTestCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst() - .orElseThrow(); + ProgrammingExerciseTestCase testCase = testCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst().orElseThrow(); Long entryId = testCase.getSolutionEntries().stream().findFirst().orElseThrow().getId(); request.delete("/api/programming-exercises/" + programmingExercise.getId() + "/test-cases/" + testCase.getId() + "/solution-entries/" + entryId, HttpStatus.FORBIDDEN); } @@ -174,8 +149,7 @@ void testDeleteAllSolutionEntriesForExercise() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") void testUpdateSolutionEntry() throws Exception { - ProgrammingExerciseTestCase testCase = programmingExerciseTestCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst() - .orElseThrow(); + ProgrammingExerciseTestCase testCase = testCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst().orElseThrow(); ProgrammingExerciseSolutionEntry entry = testCase.getSolutionEntries().stream().findFirst().orElseThrow(); Long entryId = entry.getId(); String updatedFilePath = "NewPath.java"; @@ -190,8 +164,7 @@ void testUpdateSolutionEntry() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") void testUpdateSolutionEntryWithInvalidId() throws Exception { - ProgrammingExerciseTestCase testCase = programmingExerciseTestCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst() - .orElseThrow(); + ProgrammingExerciseTestCase testCase = testCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst().orElseThrow(); ProgrammingExerciseSolutionEntry entry = testCase.getSolutionEntries().stream().findFirst().orElseThrow(); Long entryId = entry.getId(); String updatedFilePath = "NewPath.java"; @@ -206,8 +179,7 @@ void testUpdateSolutionEntryWithInvalidId() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "STUDENT") void testUpdateSolutionEntryAsStudent() throws Exception { - ProgrammingExerciseTestCase testCase = programmingExerciseTestCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst() - .orElseThrow(); + ProgrammingExerciseTestCase testCase = testCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst().orElseThrow(); ProgrammingExerciseSolutionEntry entry = testCase.getSolutionEntries().stream().findFirst().orElseThrow(); Long entryId = entry.getId(); @@ -217,8 +189,7 @@ void testUpdateSolutionEntryAsStudent() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testUpdateSolutionEntryAsTutor() throws Exception { - ProgrammingExerciseTestCase testCase = programmingExerciseTestCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst() - .orElseThrow(); + ProgrammingExerciseTestCase testCase = testCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()).stream().findFirst().orElseThrow(); ProgrammingExerciseSolutionEntry entry = testCase.getSolutionEntries().stream().findFirst().orElseThrow(); Long entryId = entry.getId(); @@ -259,7 +230,7 @@ void testCreateManualSolutionEntry() throws Exception { manualEntry.setLine(1); manualEntry.setFilePath("src/de/tum/in/ase/BubbleSort.java"); - var testCase = programmingExerciseTestCaseRepository.findByExerciseId(programmingExercise.getId()).stream().findFirst().orElseThrow(); + var testCase = testCaseRepository.findByExerciseId(programmingExercise.getId()).stream().findFirst().orElseThrow(); manualEntry.setTestCase(testCase); request.postWithoutLocation("/api/programming-exercises/" + programmingExercise.getId() + "/test-cases/" + testCase.getId() + "/solution-entries", manualEntry, diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseTaskIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseTaskIntegrationTest.java index c302c5945314..c4def2a98eb6 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseTaskIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseTaskIntegrationTest.java @@ -10,46 +10,21 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.DomainObject; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationIndependentTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseSolutionEntry; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTask; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseSolutionEntryRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository; -import de.tum.cit.aet.artemis.programming.service.hestia.ProgrammingExerciseTaskService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class ProgrammingExerciseTaskIntegrationTest extends AbstractSpringIntegrationIndependentTest { +class ProgrammingExerciseTaskIntegrationTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "progextask"; - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseTaskRepository programmingExerciseTaskRepository; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository programmingExerciseTestCaseRepository; - - @Autowired - private ProgrammingExerciseSolutionEntryRepository programmingExerciseSolutionEntryRepository; - - @Autowired - private ProgrammingExerciseTaskService programmingExerciseTaskService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - private ProgrammingExercise programmingExercise; private Set testCases; @@ -60,7 +35,7 @@ void initTestCases() { final Course course = programmingExerciseUtilService.addCourseWithOneProgrammingExerciseAndSpecificTestCases(); programmingExercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); - this.testCases = programmingExerciseTestCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()); + this.testCases = testCaseRepository.findByExerciseIdWithSolutionEntries(programmingExercise.getId()); for (ProgrammingExerciseTestCase testCase : testCases) { var solutionEntry = new ProgrammingExerciseSolutionEntry(); solutionEntry.setTestCase(testCase); @@ -102,10 +77,10 @@ void testDeleteAllTasksAndSolutionEntriesForProgrammingExercise() throws Excepti task.setExercise(programmingExercise); task.setTaskName("Task"); task.setTestCases(new HashSet<>(testCases)); - programmingExerciseTaskRepository.save(task); + taskRepository.save(task); request.delete("/api/programming-exercises/" + programmingExercise.getId() + "/tasks", HttpStatus.NO_CONTENT); - assertThat(programmingExerciseTaskRepository.findByExerciseId(programmingExercise.getId())).isEmpty(); + assertThat(taskRepository.findByExerciseId(programmingExercise.getId())).isEmpty(); assertThat(programmingExerciseSolutionEntryRepository.findAllById(solutionEntryIdsBeforeDeleting)).isEmpty(); } @@ -139,7 +114,7 @@ void testTaskExtractionForProgrammingExercise() throws Exception { programmingExerciseTaskService.updateTasksFromProblemStatement(programmingExercise); request.get("/api/programming-exercises/" + programmingExercise.getId() + "/tasks", HttpStatus.OK, Set.class); - Set extractedTasks = programmingExerciseTaskRepository.findByExerciseIdWithTestCaseAndSolutionEntriesElseThrow(programmingExercise.getId()); + Set extractedTasks = taskRepository.findByExerciseIdWithTestCaseAndSolutionEntriesElseThrow(programmingExercise.getId()); Optional task1Optional = extractedTasks.stream().filter(task -> task.getTaskName().equals(taskName1)).findFirst(); Optional task2Optional = extractedTasks.stream().filter(task -> task.getTaskName().equals(taskName2)).findFirst(); assertThat(task1Optional).isPresent(); @@ -164,7 +139,7 @@ void testTaskExtractionForEmptyProblemStatement() throws Exception { request.get("/api/programming-exercises/" + programmingExercise.getId() + "/tasks", HttpStatus.OK, Set.class); - assertThat(programmingExerciseTaskRepository.findByExerciseId(programmingExercise.getId())).isEmpty(); + assertThat(taskRepository.findByExerciseId(programmingExercise.getId())).isEmpty(); } @Test diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseTaskServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseTaskServiceTest.java index b2fcb4b0bf31..a322fa44ba44 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseTaskServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseTaskServiceTest.java @@ -9,53 +9,20 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.DomainObject; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationIndependentTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.hestia.CodeHint; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTask; -import de.tum.cit.aet.artemis.programming.repository.hestia.CodeHintRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository; -import de.tum.cit.aet.artemis.programming.service.hestia.ProgrammingExerciseTaskService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class ProgrammingExerciseTaskServiceTest extends AbstractSpringIntegrationIndependentTest { +class ProgrammingExerciseTaskServiceTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "progextaskservice"; - @Autowired - private ProgrammingExerciseTaskService programmingExerciseTaskService; - - @Autowired - private ProgrammingExerciseTaskRepository programmingExerciseTaskRepository; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository programmingExerciseTestCaseRepository; - - @Autowired - private CodeHintRepository codeHintRepository; - - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - private ProgrammingExercise programmingExercise; @BeforeEach @@ -78,24 +45,23 @@ private void updateProblemStatement(String problemStatement) { @Test void testNewExercise() { - assertThat(programmingExerciseTaskRepository.findByExerciseId(programmingExercise.getId())).hasSize(2); - var tasks = programmingExerciseTaskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()); + assertThat(taskRepository.findByExerciseId(programmingExercise.getId())).hasSize(2); + var tasks = taskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()); assertThat(tasks).hasSize(2).anyMatch(programmingExerciseTask -> checkTaskEqual(programmingExerciseTask, "Task 1", "testClass[BubbleSort]")) .anyMatch(programmingExerciseTask -> checkTaskEqual(programmingExerciseTask, "Task 2", "testMethods[Context]")); } @Test void testAddTask() { - var previousTaskIds = programmingExerciseTaskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()).stream().map(ProgrammingExerciseTask::getId) - .collect(Collectors.toSet()); + var previousTaskIds = taskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()).stream().map(ProgrammingExerciseTask::getId).collect(Collectors.toSet()); updateProblemStatement(""" [task][Task 1](testClass[BubbleSort]) [task][Task 2](testMethods[Context]) [task][Task 3](testMethods[Policy]) """); - assertThat(programmingExerciseTaskRepository.findByExerciseId(programmingExercise.getId())).hasSize(3); - var tasks = programmingExerciseTaskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()); + assertThat(taskRepository.findByExerciseId(programmingExercise.getId())).hasSize(3); + var tasks = taskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()); assertThat(tasks).hasSize(3).anyMatch(programmingExerciseTask -> checkTaskEqual(programmingExerciseTask, "Task 1", "testClass[BubbleSort]")) .anyMatch(programmingExerciseTask -> checkTaskEqual(programmingExerciseTask, "Task 2", "testMethods[Context]")) .anyMatch(programmingExerciseTask -> checkTaskEqual(programmingExerciseTask, "Task 3", "testMethods[Policy]")); @@ -108,14 +74,14 @@ void testAddTask() { @Test void testRemoveAllTasks() { updateProblemStatement("Empty"); - assertThat(programmingExerciseTaskRepository.findByExerciseId(programmingExercise.getId())).isEmpty(); + assertThat(taskRepository.findByExerciseId(programmingExercise.getId())).isEmpty(); } @Test void testReduceToOneTask() { updateProblemStatement("[task][Task 1](testClass[BubbleSort],testMethods[Context], testMethods[Policy])"); - assertThat(programmingExerciseTaskRepository.findByExerciseId(programmingExercise.getId())).hasSize(1); - var tasks = programmingExerciseTaskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()); + assertThat(taskRepository.findByExerciseId(programmingExercise.getId())).hasSize(1); + var tasks = taskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()); assertThat(tasks).hasSize(1); var task = tasks.stream().findFirst().orElseThrow(); assertThat(task.getTaskName()).isEqualTo("Task 1"); @@ -130,21 +96,20 @@ void testReduceToOneTask() { */ @Test void testRenameTask() { - var previousTaskIds = programmingExerciseTaskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()).stream().map(ProgrammingExerciseTask::getId) - .collect(Collectors.toSet()); + var previousTaskIds = taskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()).stream().map(ProgrammingExerciseTask::getId).collect(Collectors.toSet()); updateProblemStatement(""" [task][Task 1a](testClass[BubbleSort]) [task][Task 2](testMethods[Context]) """); - assertThat(programmingExerciseTaskRepository.findByExerciseId(programmingExercise.getId())).hasSize(2); - var tasks = programmingExerciseTaskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()); + assertThat(taskRepository.findByExerciseId(programmingExercise.getId())).hasSize(2); + var tasks = taskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()); var newTaskIds = tasks.stream().map(ProgrammingExerciseTask::getId).collect(Collectors.toSet()); assertThat(previousTaskIds).isEqualTo(newTaskIds); - assertThat(programmingExerciseTaskRepository.findByExerciseIdWithTestCases(programmingExercise.getId())).isEqualTo(tasks); + assertThat(taskRepository.findByExerciseIdWithTestCases(programmingExercise.getId())).isEqualTo(tasks); assertThat(tasks).anyMatch(programmingExerciseTask -> checkTaskEqual(programmingExerciseTask, "Task 1a", "testClass[BubbleSort]")) .anyMatch(programmingExerciseTask -> checkTaskEqual(programmingExerciseTask, "Task 2", "testMethods[Context]")); @@ -155,8 +120,7 @@ void testRenameTask() { */ @Test void testNoChanges() { - var previousTaskIds = programmingExerciseTaskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()).stream().map(ProgrammingExerciseTask::getId) - .collect(Collectors.toSet()); + var previousTaskIds = taskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()).stream().map(ProgrammingExerciseTask::getId).collect(Collectors.toSet()); updateProblemStatement(""" Test @@ -164,17 +128,15 @@ void testNoChanges() { [task][Task 2](testMethods[Context]) """); - assertThat(programmingExerciseTaskRepository.findByExerciseId(programmingExercise.getId())).hasSize(2); + assertThat(taskRepository.findByExerciseId(programmingExercise.getId())).hasSize(2); - var newTaskIds = programmingExerciseTaskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()).stream().map(ProgrammingExerciseTask::getId) - .collect(Collectors.toSet()); + var newTaskIds = taskRepository.findByExerciseIdWithTestCases(programmingExercise.getId()).stream().map(ProgrammingExerciseTask::getId).collect(Collectors.toSet()); assertThat(previousTaskIds).isEqualTo(newTaskIds); } @Test void testDeleteWithCodeHints() { - var task = programmingExerciseTaskRepository.findByExerciseId(programmingExercise.getId()).stream().filter(task1 -> "Task 1".equals(task1.getTaskName())).findFirst() - .orElse(null); + var task = taskRepository.findByExerciseId(programmingExercise.getId()).stream().filter(task1 -> "Task 1".equals(task1.getTaskName())).findFirst().orElse(null); assertThat(task).isNotNull(); var codeHint = new CodeHint(); @@ -183,8 +145,8 @@ void testDeleteWithCodeHints() { codeHintRepository.save(codeHint); programmingExerciseTaskService.delete(task); - assertThat(programmingExerciseTaskRepository.findByExerciseId(programmingExercise.getId())).hasSize(1); - assertThat(programmingExerciseTaskRepository.findById(task.getId())).isEmpty(); + assertThat(taskRepository.findByExerciseId(programmingExercise.getId())).hasSize(1); + assertThat(taskRepository.findById(task.getId())).isEmpty(); assertThat(codeHintRepository.findByExerciseId(programmingExercise.getId())).isEmpty(); } @@ -194,7 +156,7 @@ void getTasksWithoutInactiveFiltersOutInactive() { programmingExercise = programmingExerciseRepository .findByIdWithEagerTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxReposAndBuildConfig(programmingExercise.getId()) .orElseThrow(); - programmingExerciseTestCaseRepository.deleteAll(programmingExercise.getTestCases()); + testCaseRepository.deleteAll(programmingExercise.getTestCases()); String[] testCaseNames = { "testClass[BubbleSort]", "testParametrized(Parameter1, 2)[1]" }; for (var name : testCaseNames) { @@ -202,14 +164,14 @@ void getTasksWithoutInactiveFiltersOutInactive() { testCase.setExercise(programmingExercise); testCase.setTestName(name); testCase.setActive(true); - programmingExerciseTestCaseRepository.save(testCase); + testCaseRepository.save(testCase); } var testCase = new ProgrammingExerciseTestCase(); testCase.setExercise(programmingExercise); testCase.setTestName("testWithBraces()"); testCase.setActive(false); - programmingExerciseTestCaseRepository.save(testCase); + testCaseRepository.save(testCase); programmingExercise = programmingExerciseRepository .findByIdWithEagerTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxReposAndBuildConfig(programmingExercise.getId()) @@ -232,7 +194,7 @@ void testParseTestCaseNames() { programmingExercise = programmingExerciseRepository .findByIdWithEagerTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxReposAndBuildConfig(programmingExercise.getId()) .orElseThrow(); - programmingExerciseTestCaseRepository.deleteAll(programmingExercise.getTestCases()); + testCaseRepository.deleteAll(programmingExercise.getTestCases()); String[] testCaseNames = new String[] { "testClass[BubbleSort]", "testWithBraces()", "testParametrized(Parameter1, 2)[1]" }; for (var name : testCaseNames) { @@ -240,7 +202,7 @@ void testParseTestCaseNames() { testCase.setExercise(programmingExercise); testCase.setTestName(name); testCase.setActive(true); - programmingExerciseTestCaseRepository.save(testCase); + testCaseRepository.save(testCase); } programmingExercise = programmingExerciseRepository .findByIdWithEagerTestCasesStaticCodeAnalysisCategoriesHintsAndTemplateAndSolutionParticipationsAndAuxReposAndBuildConfig(programmingExercise.getId()) @@ -250,10 +212,10 @@ void testParseTestCaseNames() { [task][Task 1](testClass[BubbleSort],testWithBraces(),testParametrized(Parameter1, 2)[1]) """); - var actualTasks = programmingExerciseTaskRepository.findByExerciseId(programmingExercise.getId()); + var actualTasks = taskRepository.findByExerciseId(programmingExercise.getId()); assertThat(actualTasks).hasSize(1); final var actualTask = actualTasks.iterator().next().getId(); - var actualTaskWithTestCases = programmingExerciseTaskRepository.findByIdWithTestCaseAndSolutionEntriesElseThrow(actualTask); + var actualTaskWithTestCases = taskRepository.findByIdWithTestCaseAndSolutionEntriesElseThrow(actualTask); assertThat(actualTaskWithTestCases.getTaskName()).isEqualTo("Task 1"); var actualTestCaseNames = actualTaskWithTestCases.getTestCases().stream().map(ProgrammingExerciseTestCase::getTestName).toList(); assertThat(actualTestCaseNames).containsExactlyInAnyOrder(testCaseNames); @@ -262,15 +224,15 @@ void testParseTestCaseNames() { @Test @WithMockUser(username = "instructor1", roles = "INSTRUCTOR") void testExtractTasksFromTestIds() { - var test1 = programmingExerciseTestCaseRepository.findByExerciseIdAndTestName(programmingExercise.getId(), "testClass[BubbleSort]").orElseThrow(); - var test2 = programmingExerciseTestCaseRepository.findByExerciseIdAndTestName(programmingExercise.getId(), "testMethods[Context]").orElseThrow(); + var test1 = testCaseRepository.findByExerciseIdAndTestName(programmingExercise.getId(), "testClass[BubbleSort]").orElseThrow(); + var test2 = testCaseRepository.findByExerciseIdAndTestName(programmingExercise.getId(), "testMethods[Context]").orElseThrow(); updateProblemStatement("[task][Task 1](%s,%s)".formatted(test1.getId(), test2.getId())); - var actualTasks = programmingExerciseTaskRepository.findByExerciseId(programmingExercise.getId()); + var actualTasks = taskRepository.findByExerciseId(programmingExercise.getId()); assertThat(actualTasks).hasSize(1); final var actualTask = actualTasks.iterator().next().getId(); - var actualTaskWithTestCases = programmingExerciseTaskRepository.findByIdWithTestCaseAndSolutionEntriesElseThrow(actualTask); + var actualTaskWithTestCases = taskRepository.findByIdWithTestCaseAndSolutionEntriesElseThrow(actualTask); assertThat(actualTaskWithTestCases.getTaskName()).isEqualTo("Task 1"); var actualTestCaseNames = actualTaskWithTestCases.getTestCases().stream().map(ProgrammingExerciseTestCase::getTestName).toList(); assertThat(actualTestCaseNames).containsExactlyInAnyOrder("testClass[BubbleSort]", "testMethods[Context]"); @@ -283,7 +245,7 @@ private boolean checkTaskEqual(ProgrammingExerciseTask task, String expectedName @Test void testNameReplacement() { - Map testCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExercise.getId()).stream() + Map testCases = testCaseRepository.findByExerciseId(programmingExercise.getId()).stream() .collect(Collectors.toMap(ProgrammingExerciseTestCase::getTestName, ProgrammingExerciseTestCase::getId)); programmingExerciseTaskService.replaceTestNamesWithIds(programmingExercise); @@ -300,9 +262,9 @@ void testNameReplacement() { void testNameReplacementKeepsInactiveTests() { // Task 1 is inactive, task 2 does not exist updateProblemStatement("[task][Task 1](testClass[BubbleSort])\n[task][Task 2](nonExistingTask)"); - var testCase = programmingExerciseTestCaseRepository.findByExerciseIdAndTestName(programmingExercise.getId(), "testClass[BubbleSort]").orElseThrow(); + var testCase = testCaseRepository.findByExerciseIdAndTestName(programmingExercise.getId(), "testClass[BubbleSort]").orElseThrow(); testCase.setActive(false); - programmingExerciseTestCaseRepository.save(testCase); + testCaseRepository.save(testCase); programmingExerciseTaskService.replaceTestNamesWithIds(programmingExercise); String problemStatement = programmingExercise.getProblemStatement(); @@ -313,7 +275,7 @@ void testNameReplacementKeepsInactiveTests() { @Test void testNameReplacementSpecialNames() { - var bubbleSort = programmingExerciseTestCaseRepository.findByExerciseIdAndTestName(programmingExercise.getId(), "testClass[BubbleSort]").orElseThrow(); + var bubbleSort = testCaseRepository.findByExerciseIdAndTestName(programmingExercise.getId(), "testClass[BubbleSort]").orElseThrow(); var braces = programmingExerciseUtilService.addTestCaseToProgrammingExercise(programmingExercise, "testWithBraces()"); var parameterized = programmingExerciseUtilService.addTestCaseToProgrammingExercise(programmingExercise, "testParametrized(Parameter1, 2)[1]"); updateProblemStatement(""" @@ -414,10 +376,10 @@ class LinkedList { @Test void testIdReplacementWithNames() { - var bubbleSort = programmingExerciseTestCaseRepository.findByExerciseIdAndTestName(programmingExercise.getId(), "testClass[BubbleSort]").orElseThrow(); + var bubbleSort = testCaseRepository.findByExerciseIdAndTestName(programmingExercise.getId(), "testClass[BubbleSort]").orElseThrow(); var inactiveTest = programmingExerciseUtilService.addTestCaseToProgrammingExercise(programmingExercise, "testName"); inactiveTest.setActive(false); - programmingExerciseTestCaseRepository.save(inactiveTest); + testCaseRepository.save(inactiveTest); updateProblemStatement("[task][Taskname](%s,%s)".formatted(bubbleSort.getId(), inactiveTest.getId())); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/StructuralTestCaseServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/StructuralTestCaseServiceTest.java index 08d6f5994ac7..747032881267 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/StructuralTestCaseServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/StructuralTestCaseServiceTest.java @@ -9,21 +9,15 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.assessment.domain.Visibility; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; -import de.tum.cit.aet.artemis.core.util.CourseUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTestCaseType; -import de.tum.cit.aet.artemis.programming.hestia.util.HestiaUtilTestService; -import de.tum.cit.aet.artemis.programming.icl.AbstractLocalCILocalVCIntegrationTest; import de.tum.cit.aet.artemis.programming.service.hestia.structural.StructuralSolutionEntryGenerationException; -import de.tum.cit.aet.artemis.programming.service.hestia.structural.StructuralTestCaseService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; import de.tum.cit.aet.artemis.programming.util.LocalRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; @@ -31,29 +25,14 @@ * Tests for the StructuralTestCaseService * Test if solution entries are generated as expected for structural tests */ -class StructuralTestCaseServiceTest extends AbstractLocalCILocalVCIntegrationTest { +class StructuralTestCaseServiceTest extends AbstractProgrammingIntegrationLocalCILocalVCTestBase { private static final String TEST_PREFIX = "structuraltestcaseservice"; - @Autowired - private CourseUtilService courseUtilService; - - @Autowired - private UserUtilService userUtilService; - private final LocalRepository solutionRepo = new LocalRepository("main"); private final LocalRepository testRepo = new LocalRepository("main"); - @Autowired - private HestiaUtilTestService hestiaUtilTestService; - - @Autowired - private StructuralTestCaseService structuralTestCaseService; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository testCaseRepository; - private ProgrammingExercise exercise; @Override diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/TestwiseCoverageIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/TestwiseCoverageIntegrationTest.java index 6319f8395fd4..a9544af19d89 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/TestwiseCoverageIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/TestwiseCoverageIntegrationTest.java @@ -7,11 +7,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationIndependentTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; @@ -19,40 +19,11 @@ import de.tum.cit.aet.artemis.programming.domain.hestia.CoverageFileReport; import de.tum.cit.aet.artemis.programming.domain.hestia.CoverageReport; import de.tum.cit.aet.artemis.programming.domain.hestia.TestwiseCoverageReportEntry; -import de.tum.cit.aet.artemis.programming.repository.SolutionProgrammingExerciseParticipationRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.CoverageFileReportRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.CoverageReportRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.TestwiseCoverageReportEntryRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class TestwiseCoverageIntegrationTest extends AbstractSpringIntegrationIndependentTest { +class TestwiseCoverageIntegrationTest extends AbstractProgrammingIntegrationIndependentTest { private static final String TEST_PREFIX = "testwisecoverageint"; - @Autowired - private ProgrammingExerciseTestCaseTestRepository programmingExerciseTestCaseRepository; - - @Autowired - private CoverageReportRepository coverageReportRepository; - - @Autowired - private CoverageFileReportRepository coverageFileReportRepository; - - @Autowired - private TestwiseCoverageReportEntryRepository testwiseCoverageReportEntryRepository; - - @Autowired - private ProgrammingSubmissionTestRepository programmingSubmissionRepository; - - @Autowired - private SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - private ProgrammingExercise programmingExercise; private ProgrammingSubmission latestSolutionSubmission; @@ -64,7 +35,7 @@ void setup() { userUtilService.addUsers(TEST_PREFIX, 1, 1, 0, 0); final Course course = programmingExerciseUtilService.addCourseWithOneProgrammingExercise(false, true, ProgrammingLanguage.JAVA); programmingExercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); - var solutionParticipation = solutionProgrammingExerciseRepository.findWithEagerResultsAndSubmissionsByProgrammingExerciseId(programmingExercise.getId()).orElseThrow(); + var solutionParticipation = solutionEntryRepository.findWithEagerResultsAndSubmissionsByProgrammingExerciseId(programmingExercise.getId()).orElseThrow(); var unsavedPreviousSubmission = new ProgrammingSubmission(); unsavedPreviousSubmission.setParticipation(solutionParticipation); unsavedPreviousSubmission.setSubmissionDate(ZonedDateTime.of(2022, 4, 5, 12, 0, 0, 0, ZoneId.of("Europe/Berlin"))); @@ -74,8 +45,8 @@ void setup() { unsavedLatestSubmission.setSubmissionDate(ZonedDateTime.of(2022, 4, 5, 13, 0, 0, 0, ZoneId.of("Europe/Berlin"))); latestSolutionSubmission = programmingSubmissionRepository.save(unsavedLatestSubmission); - var testCase1 = programmingExerciseTestCaseRepository.save(new ProgrammingExerciseTestCase().exercise(programmingExercise).testName("test1()")); - var testCase2 = programmingExerciseTestCaseRepository.save(new ProgrammingExerciseTestCase().exercise(programmingExercise).testName("test2()")); + var testCase1 = testCaseRepository.save(new ProgrammingExerciseTestCase().exercise(programmingExercise).testName("test1()")); + var testCase2 = testCaseRepository.save(new ProgrammingExerciseTestCase().exercise(programmingExercise).testName("test2()")); generateAndSaveSimpleReport(0.3, "src/de/tum/in/ase/BubbleSort.java", 15, 5, 1, 5, testCase1, previousSolutionSubmission); latestReport = generateAndSaveSimpleReport(0.4, "src/de/tum/in/ase/BubbleSort.java", 20, 8, 1, 8, testCase2, latestSolutionSubmission); } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/TestwiseCoverageReportServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/TestwiseCoverageReportServiceTest.java index ba89b52dc804..18addd11453e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/TestwiseCoverageReportServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/TestwiseCoverageReportServiceTest.java @@ -9,60 +9,23 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; import de.tum.cit.aet.artemis.programming.domain.hestia.TestwiseCoverageReportEntry; -import de.tum.cit.aet.artemis.programming.hestia.util.HestiaUtilTestService; import de.tum.cit.aet.artemis.programming.hestia.util.TestwiseCoverageTestUtil; -import de.tum.cit.aet.artemis.programming.icl.AbstractLocalCILocalVCIntegrationTest; -import de.tum.cit.aet.artemis.programming.repository.SolutionProgrammingExerciseParticipationRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.CoverageReportRepository; -import de.tum.cit.aet.artemis.programming.service.hestia.TestwiseCoverageService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.util.LocalRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -class TestwiseCoverageReportServiceTest extends AbstractLocalCILocalVCIntegrationTest { +class TestwiseCoverageReportServiceTest extends AbstractProgrammingIntegrationLocalCILocalVCTestBase { private static final String TEST_PREFIX = "testwisecoveragereportservice"; - @Autowired - private TestwiseCoverageService testwiseCoverageService; - - @Autowired - private CoverageReportRepository coverageReportRepository; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository programmingExerciseTestCaseRepository; - - @Autowired - private SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseRepository; - - @Autowired - private HestiaUtilTestService hestiaUtilTestService; - - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - private ProgrammingExercise programmingExercise; private ProgrammingSubmission solutionSubmission; @@ -85,9 +48,9 @@ void setup() throws Exception { solutionRepo); var testCase1 = new ProgrammingExerciseTestCase().testName("test1()").exercise(programmingExercise).active(true).weight(1.0); - programmingExerciseTestCaseRepository.save(testCase1); + testCaseRepository.save(testCase1); var testCase2 = new ProgrammingExerciseTestCase().testName("test2()").exercise(programmingExercise).active(true).weight(1.0); - programmingExerciseTestCaseRepository.save(testCase2); + testCaseRepository.save(testCase2); var solutionParticipation = solutionProgrammingExerciseRepository.findWithEagerResultsAndSubmissionsByProgrammingExerciseId(programmingExercise.getId()).orElseThrow(); solutionSubmission = programmingExerciseUtilService.createProgrammingSubmission(solutionParticipation, false); programmingExercise = programmingExerciseRepository.findByIdElseThrow(programmingExercise.getId()); @@ -110,7 +73,7 @@ void shouldCreateFullTestwiseCoverageReport() { // 18/50 lines covered = 32% assertThat(report.getCoveredLineRatio()).isEqualTo(0.32); - var testCases = programmingExerciseTestCaseRepository.findByExerciseId(programmingExercise.getId()); + var testCases = testCaseRepository.findByExerciseId(programmingExercise.getId()); var testCase1 = testCases.stream().filter(testCase -> "test1()".equals(testCase.getTestName())).findFirst().orElseThrow(); var testCase2 = testCases.stream().filter(testCase -> "test2()".equals(testCase.getTestName())).findFirst().orElseThrow(); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/behavioral/BehavioralTestCaseServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/behavioral/BehavioralTestCaseServiceLocalCILocalVCTest.java similarity index 74% rename from src/test/java/de/tum/cit/aet/artemis/programming/hestia/behavioral/BehavioralTestCaseServiceTest.java rename to src/test/java/de/tum/cit/aet/artemis/programming/hestia/behavioral/BehavioralTestCaseServiceLocalCILocalVCTest.java index f646dd652ded..d4de36c789e9 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/behavioral/BehavioralTestCaseServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/behavioral/BehavioralTestCaseServiceLocalCILocalVCTest.java @@ -8,13 +8,11 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.assessment.domain.Visibility; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; @@ -25,57 +23,14 @@ import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseSolutionEntry; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTestCaseType; import de.tum.cit.aet.artemis.programming.domain.hestia.TestwiseCoverageReportEntry; -import de.tum.cit.aet.artemis.programming.hestia.util.HestiaUtilTestService; -import de.tum.cit.aet.artemis.programming.icl.AbstractLocalCILocalVCIntegrationTest; -import de.tum.cit.aet.artemis.programming.repository.SolutionProgrammingExerciseParticipationRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.CoverageFileReportRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.CoverageReportRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseGitDiffReportRepository; -import de.tum.cit.aet.artemis.programming.repository.hestia.TestwiseCoverageReportEntryRepository; -import de.tum.cit.aet.artemis.programming.service.hestia.behavioral.BehavioralTestCaseService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; import de.tum.cit.aet.artemis.programming.util.LocalRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -class BehavioralTestCaseServiceTest extends AbstractLocalCILocalVCIntegrationTest { +class BehavioralTestCaseServiceLocalCILocalVCTest extends AbstractProgrammingIntegrationLocalCILocalVCTestBase { private static final String TEST_PREFIX = "behavioraltestcastservice"; private final LocalRepository solutionRepo = new LocalRepository("main"); - @Autowired - private BehavioralTestCaseService behavioralTestCaseService; - - @Autowired - private HestiaUtilTestService hestiaUtilTestService; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository testCaseRepository; - - @Autowired - private ProgrammingExerciseGitDiffReportRepository programmingExerciseGitDiffReportRepository; - - @Autowired - private SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseRepository; - - @Autowired - private CoverageReportRepository coverageReportRepository; - - @Autowired - private CoverageFileReportRepository coverageFileReportRepository; - - @Autowired - private TestwiseCoverageReportEntryRepository testwiseCoverageReportEntryRepository; - - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - private ProgrammingExercise exercise; @Override @@ -113,7 +68,7 @@ private ProgrammingExerciseGitDiffReport newGitDiffReport() { gitDiffReport.setProgrammingExercise(exercise); gitDiffReport.setSolutionRepositoryCommitHash("123a"); gitDiffReport.setTemplateRepositoryCommitHash("123b"); - gitDiffReport = programmingExerciseGitDiffReportRepository.save(gitDiffReport); + gitDiffReport = reportRepository.save(gitDiffReport); return gitDiffReport; } @@ -124,7 +79,7 @@ private ProgrammingExerciseGitDiffReport addGitDiffEntry(String filePath, int st gitDiffEntry.setLineCount(lineCount); gitDiffEntry.setGitDiffReport(gitDiffReport); gitDiffReport.getEntries().add(gitDiffEntry); - return programmingExerciseGitDiffReportRepository.save(gitDiffReport); + return reportRepository.save(gitDiffReport); } private CoverageReport newCoverageReport() { diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java index 4563d7c9f95c..0be99f7c9aa1 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java @@ -40,9 +40,6 @@ import org.junit.jupiter.api.parallel.ExecutionMode; import org.mockito.ArgumentMatcher; import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -53,7 +50,6 @@ import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.Frame; import com.hazelcast.collection.IQueue; -import com.hazelcast.core.HazelcastInstance; import com.hazelcast.map.IMap; import de.tum.cit.aet.artemis.assessment.domain.Result; @@ -62,15 +58,12 @@ import de.tum.cit.aet.artemis.core.exception.VersionControlException; import de.tum.cit.aet.artemis.exercise.domain.ExerciseMode; import de.tum.cit.aet.artemis.exercise.domain.Team; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; import de.tum.cit.aet.artemis.programming.domain.RepositoryType; import de.tum.cit.aet.artemis.programming.domain.build.BuildJob; import de.tum.cit.aet.artemis.programming.domain.build.BuildStatus; -import de.tum.cit.aet.artemis.programming.service.BuildLogEntryService; -import de.tum.cit.aet.artemis.programming.service.ParticipationVcsAccessTokenService; -import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCServletService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository; import de.tum.cit.aet.artemis.programming.util.LocalRepository; // TestInstance.Lifecycle.PER_CLASS allows all test methods in this class to share the same instance of the test class. @@ -84,32 +77,10 @@ // concurrently. For example, it prevents overloading the LocalCI's result processing system with too many build job results at the same time, which could lead to flaky tests // or timeouts. By keeping everything in the same thread, we maintain more predictable and stable test behavior, while not increasing the test execution time significantly. @Execution(ExecutionMode.SAME_THREAD) -class LocalCIIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { +class LocalCIIntegrationTest extends AbstractProgrammingIntegrationLocalCILocalVCTestBase { private static final String TEST_PREFIX = "localciint"; - @Autowired - private LocalVCServletService localVCServletService; - - @Autowired - private ProgrammingSubmissionTestRepository programmingSubmissionRepository; - - @Autowired - private ParticipationVcsAccessTokenService participationVcsAccessTokenService; - - @Autowired - private BuildLogEntryService buildLogEntryService; - - @Autowired - @Qualifier("hazelcastInstance") - private HazelcastInstance hazelcastInstance; - - @Value("${artemis.user-management.internal-admin.username}") - private String localVCUsername; - - @Value("${artemis.user-management.internal-admin.password}") - private String localVCPassword; - @Override protected String getTestPrefix() { return TEST_PREFIX; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java index e05b87776154..e5c7f52e1413 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java @@ -14,14 +14,11 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; import com.hazelcast.collection.IQueue; -import com.hazelcast.core.HazelcastInstance; import com.hazelcast.map.IMap; import de.tum.cit.aet.artemis.assessment.domain.AssessmentType; @@ -33,33 +30,18 @@ import de.tum.cit.aet.artemis.buildagent.dto.FinishedBuildJobDTO; import de.tum.cit.aet.artemis.buildagent.dto.JobTimingInfo; import de.tum.cit.aet.artemis.buildagent.dto.RepositoryInfo; -import de.tum.cit.aet.artemis.buildagent.service.SharedQueueProcessingService; import de.tum.cit.aet.artemis.core.dto.SortingOrder; import de.tum.cit.aet.artemis.core.dto.pageablesearch.PageableSearchDTO; -import de.tum.cit.aet.artemis.core.util.PageableSearchUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase; import de.tum.cit.aet.artemis.programming.domain.RepositoryType; import de.tum.cit.aet.artemis.programming.domain.build.BuildJob; import de.tum.cit.aet.artemis.programming.domain.build.BuildLogEntry; import de.tum.cit.aet.artemis.programming.domain.build.BuildStatus; -import de.tum.cit.aet.artemis.programming.service.BuildLogEntryService; -class LocalCIResourceIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { +class LocalCIResourceIntegrationTest extends AbstractProgrammingIntegrationLocalCILocalVCTestBase { private static final String TEST_PREFIX = "localciresourceint"; - @Autowired - @Qualifier("hazelcastInstance") - private HazelcastInstance hazelcastInstance; - - @Autowired - private SharedQueueProcessingService sharedQueueProcessingService; - - @Autowired - private BuildLogEntryService buildLogEntryService; - - @Autowired - private PageableSearchUtilService pageableSearchUtilService; - protected BuildJobQueueItem job1; protected BuildJobQueueItem job2; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResultServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResultServiceTest.java index a951d065e0cc..abba77e692d2 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResultServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResultServiceTest.java @@ -6,19 +6,15 @@ import java.util.Collections; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import de.tum.cit.aet.artemis.core.exception.LocalCIException; -import de.tum.cit.aet.artemis.programming.service.localci.LocalCIResultService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; -class LocalCIResultServiceTest extends AbstractLocalCILocalVCIntegrationTest { +class LocalCIResultServiceTest extends AbstractProgrammingIntegrationLocalCILocalVCTestBase { private static final String TEST_PREFIX = "localciresultservice"; - @Autowired - private LocalCIResultService localCIResultService; - @Override protected String getTestPrefix() { return TEST_PREFIX; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java index b935eb8d2d67..a3fbfd1b2a7b 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java @@ -11,8 +11,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.test.context.support.WithMockUser; @@ -20,56 +18,27 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.hazelcast.collection.IQueue; -import com.hazelcast.core.HazelcastInstance; import com.hazelcast.map.IMap; import de.tum.cit.aet.artemis.buildagent.dto.BuildConfig; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; import de.tum.cit.aet.artemis.buildagent.dto.JobTimingInfo; import de.tum.cit.aet.artemis.buildagent.dto.RepositoryInfo; -import de.tum.cit.aet.artemis.buildagent.service.SharedQueueProcessingService; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; import de.tum.cit.aet.artemis.programming.domain.RepositoryType; import de.tum.cit.aet.artemis.programming.dto.CheckoutDirectoriesDTO; -import de.tum.cit.aet.artemis.programming.service.BuildScriptProviderService; -import de.tum.cit.aet.artemis.programming.service.aeolus.AeolusTemplateService; import de.tum.cit.aet.artemis.programming.service.aeolus.Windfile; import de.tum.cit.aet.artemis.programming.service.ci.ContinuousIntegrationService.BuildStatus; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; -class LocalCIServiceTest extends AbstractSpringIntegrationLocalCILocalVCTest { +class LocalCIServiceTest extends AbstractProgrammingIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "localciservice"; - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - - @Autowired - private BuildScriptProviderService buildScriptProviderService; - - @Autowired - private AeolusTemplateService aeolusTemplateService; - - @Autowired - private SharedQueueProcessingService sharedQueueProcessingService; - - @Autowired - @Qualifier("hazelcastInstance") - private HazelcastInstance hazelcastInstance; - protected IQueue queuedJobs; protected IMap processingJobs; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCIntegrationTest.java index 1531a4e2649a..af998a54a556 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCIntegrationTest.java @@ -29,13 +29,14 @@ import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.service.ldap.LdapUserDto; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase; import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCRepositoryUri; import de.tum.cit.aet.artemis.programming.util.LocalRepository; /** * This class contains integration tests for edge cases pertaining to the local VC system. */ -class LocalVCIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { +class LocalVCIntegrationTest extends AbstractProgrammingIntegrationLocalCILocalVCTestBase { private static final String TEST_PREFIX = "localvcint"; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCIIntegrationTest.java index 17ca800422e9..ff5b37990d74 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCIIntegrationTest.java @@ -38,24 +38,19 @@ import org.junit.jupiter.api.parallel.ExecutionMode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import com.hazelcast.collection.IQueue; -import com.hazelcast.core.HazelcastInstance; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; -import de.tum.cit.aet.artemis.buildagent.service.SharedQueueProcessingService; import de.tum.cit.aet.artemis.core.service.ldap.LdapUserDto; import de.tum.cit.aet.artemis.exam.domain.Exam; import de.tum.cit.aet.artemis.exam.domain.ExerciseGroup; import de.tum.cit.aet.artemis.exam.domain.StudentExam; -import de.tum.cit.aet.artemis.exam.util.ExamUtilService; import de.tum.cit.aet.artemis.exercise.domain.ExerciseMode; import de.tum.cit.aet.artemis.exercise.domain.Team; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase; import de.tum.cit.aet.artemis.programming.domain.AuxiliaryRepository; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; @@ -63,8 +58,6 @@ import de.tum.cit.aet.artemis.programming.domain.build.BuildJob; import de.tum.cit.aet.artemis.programming.domain.submissionpolicy.LockRepositoryPolicy; import de.tum.cit.aet.artemis.programming.domain.submissionpolicy.SubmissionPolicy; -import de.tum.cit.aet.artemis.programming.service.localci.LocalCITriggerService; -import de.tum.cit.aet.artemis.programming.test_repository.BuildJobTestRepository; import de.tum.cit.aet.artemis.programming.util.LocalRepository; /** @@ -83,37 +76,12 @@ // concurrently. For example, it prevents overloading the LocalCI's result processing system with too many build job results at the same time, which could lead to flaky tests // or timeouts. By keeping everything in the same thread, we maintain more predictable and stable test behavior, while not increasing the test execution time significantly. @Execution(ExecutionMode.SAME_THREAD) -class LocalVCLocalCIIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { +class LocalVCLocalCIIntegrationTest extends AbstractProgrammingIntegrationLocalCILocalVCTestBase { private static final Logger log = LoggerFactory.getLogger(LocalVCLocalCIIntegrationTest.class); private static final String TEST_PREFIX = "localvcciint"; - @Autowired - private ExamUtilService examUtilService; - - @Autowired - private BuildJobTestRepository buildJobRepository; - - @Autowired - protected LocalCITriggerService localCITriggerService; - - @Autowired - private SharedQueueProcessingService sharedQueueProcessingService; - - @Autowired - @Qualifier("hazelcastInstance") - private HazelcastInstance hazelcastInstance; - - @Value("${artemis.user-management.internal-admin.username}") - private String localVCUsername; - - @Value("${artemis.user-management.internal-admin.password}") - private String localVCPassword; - - @Value("${artemis.version-control.url}") - protected String artemisVersionControlUrl; - // ---- Repository handles ---- private LocalRepository templateRepository; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCIParticipationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCIParticipationIntegrationTest.java index 7552efe686e0..a7389b52273f 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCIParticipationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCIParticipationIntegrationTest.java @@ -6,39 +6,26 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTest; import de.tum.cit.aet.artemis.programming.domain.AuthenticationMechanism; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.TemplateProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.VcsAccessLog; import de.tum.cit.aet.artemis.programming.dto.VcsAccessLogDTO; -import de.tum.cit.aet.artemis.programming.repository.VcsAccessLogRepository; import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCRepositoryUri; import de.tum.cit.aet.artemis.programming.util.LocalRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; import de.tum.cit.aet.artemis.programming.web.repository.RepositoryActionType; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; -class LocalVCLocalCIParticipationIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { +class LocalVCLocalCIParticipationIntegrationTest extends AbstractProgrammingIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "participationlocalvclocalci"; - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private VcsAccessLogRepository vcsAccessLogRepository; - - @Autowired - private ParticipationUtilService participationUtilService; - private ProgrammingExercise programmingExercise; @BeforeEach diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCServiceTest.java index 9c6c71b4d8af..dea6e48fc3bc 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCServiceTest.java @@ -4,32 +4,19 @@ import static org.mockito.Mockito.verifyNoInteractions; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.service.connectors.ConnectorHealth; import de.tum.cit.aet.artemis.exam.domain.Exam; -import de.tum.cit.aet.artemis.exam.util.ExamUtilService; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; -class LocalVCServiceTest extends AbstractSpringIntegrationLocalCILocalVCTest { +class LocalVCServiceTest extends AbstractProgrammingIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "localvcservice"; - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - - @Autowired - private ExamUtilService examUtilService; - @Test void testHealth() { ConnectorHealth health = versionControlService.health(); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java index bca0ed60beb9..fd4abf09a0d1 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java @@ -22,10 +22,8 @@ import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyPairResourceWriter; import org.apache.sshd.common.session.helpers.AbstractSession; -import org.apache.sshd.server.SshServer; import org.apache.sshd.server.session.ServerSession; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.security.test.context.support.WithMockUser; @@ -39,9 +37,6 @@ class LocalVCSshIntegrationTest extends LocalVCIntegrationTest { private static final String TEST_PREFIX = "localvcsshint"; - @Autowired - private SshServer sshServer; - @Override protected String getTestPrefix() { return TEST_PREFIX; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/MultipleHostKeyProviderTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/MultipleHostKeyProviderTest.java index a386ae6ccc2d..db3c01ca029e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/MultipleHostKeyProviderTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/MultipleHostKeyProviderTest.java @@ -8,10 +8,11 @@ import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Profile; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.MultipleHostKeyProvider; @Profile(PROFILE_LOCALVC) -class MultipleHostKeyProviderTest extends AbstractLocalCILocalVCIntegrationTest { +class MultipleHostKeyProviderTest extends AbstractProgrammingIntegrationLocalCILocalVCTestBase { private static final String TEST_PREFIX = "multiplehostkeyprovider"; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java index f5ec144c6bf0..9d88101f8fb2 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java @@ -26,17 +26,13 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; -import de.tum.cit.aet.artemis.atlas.competency.util.CompetencyUtilService; import de.tum.cit.aet.artemis.atlas.domain.competency.Competency; -import de.tum.cit.aet.artemis.core.connector.AeolusRequestMockProvider; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTest; import de.tum.cit.aet.artemis.programming.domain.AeolusTarget; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; @@ -47,8 +43,6 @@ import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCRepositoryUri; import de.tum.cit.aet.artemis.programming.util.LocalRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; // TestInstance.Lifecycle.PER_CLASS allows all test methods in this class to share the same instance of the test class. // This reduces the overhead of repeatedly creating and tearing down a new Spring application context for each test method. @@ -61,22 +55,10 @@ // concurrently. For example, it prevents overloading the LocalCI's result processing system with too many build job results at the same time, which could lead to flaky tests // or timeouts. By keeping everything in the same thread, we maintain more predictable and stable test behavior, while not increasing the test execution time significantly. @Execution(ExecutionMode.SAME_THREAD) -class ProgrammingExerciseLocalVCLocalCIIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { +class ProgrammingExerciseLocalVCLocalCIIntegrationTest extends AbstractProgrammingIntegrationLocalCILocalVCTest { private static final String TEST_PREFIX = "progexlocalvclocalci"; - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - - @Autowired - private AeolusRequestMockProvider aeolusRequestMockProvider; - - @Autowired - private CompetencyUtilService competencyUtilService; - private Course course; private ProgrammingExercise programmingExercise; @@ -91,12 +73,6 @@ class ProgrammingExerciseLocalVCLocalCIIntegrationTest extends AbstractSpringInt private Competency competency; - @Value("${artemis.user-management.internal-admin.username}") - private String localVCUsername; - - @Value("${artemis.user-management.internal-admin.password}") - private String localVCPassword; - @BeforeAll void setupAll() { CredentialsProvider.setDefault(new UsernamePasswordCredentialsProvider(localVCUsername, localVCPassword)); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/SharedQueueManagementServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/SharedQueueManagementServiceTest.java index e0a20b1bed46..f61ff0bce53e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/SharedQueueManagementServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/SharedQueueManagementServiceTest.java @@ -5,24 +5,13 @@ import java.time.ZonedDateTime; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import com.hazelcast.core.HazelcastInstance; import com.hazelcast.map.IMap; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTest; import de.tum.cit.aet.artemis.programming.domain.build.BuildJob; -import de.tum.cit.aet.artemis.programming.service.localci.SharedQueueManagementService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; -class SharedQueueManagementServiceTest extends AbstractSpringIntegrationLocalCILocalVCTest { - - @Autowired - private SharedQueueManagementService sharedQueueManagementService; - - @Autowired - @Qualifier("hazelcastInstance") - private HazelcastInstance hazelcastInstance; +class SharedQueueManagementServiceTest extends AbstractProgrammingIntegrationLocalCILocalVCTest { @Test void testPushDockerImageCleanupInfo() { diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/BuildLogEntryServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/BuildLogEntryServiceTest.java index 04f7854a5b2a..e74c88fff158 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/service/BuildLogEntryServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/BuildLogEntryServiceTest.java @@ -12,13 +12,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationIndependentTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; import de.tum.cit.aet.artemis.programming.domain.build.BuildLogEntry; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class BuildLogEntryServiceTest extends AbstractSpringIntegrationIndependentTest { +class BuildLogEntryServiceTest extends AbstractProgrammingIntegrationIndependentTest { private static final String GRADLE_SCENARIO = """ ~~~~~~~~~~~~~~~~~~~~ Pull image progress: Downloading ~~~~~~~~~~~~~~~~~~~~ @@ -331,9 +330,6 @@ class BuildLogEntryServiceTest extends AbstractSpringIntegrationIndependentTest Finished building MTCTSTMVN-ARTEMISADMIN-JOB1-7. """; - @Autowired - private BuildLogEntryService buildLogEntryService; - @ValueSource(strings = { GRADLE_SCENARIO, MAVEN_SCENARIO }) @ParameterizedTest void testScenario(String scenario) { diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/GitlabCIServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/GitlabCIServiceTest.java index e91e0d1fdb83..fda58121a3ac 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/service/GitlabCIServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/GitlabCIServiceTest.java @@ -11,7 +11,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import java.net.URL; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; @@ -23,69 +22,24 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.exception.GitLabCIException; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; import de.tum.cit.aet.artemis.exercise.domain.participation.Participation; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; -import de.tum.cit.aet.artemis.exercise.test_repository.ParticipationTestRepository; -import de.tum.cit.aet.artemis.exercise.util.ExerciseUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationGitlabCIGitlabSamlTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; import de.tum.cit.aet.artemis.programming.domain.ProjectType; import de.tum.cit.aet.artemis.programming.domain.build.BuildLogEntry; -import de.tum.cit.aet.artemis.programming.repository.BuildLogStatisticsEntryRepository; -import de.tum.cit.aet.artemis.programming.repository.BuildPlanRepository; -import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.cit.aet.artemis.programming.service.ci.ContinuousIntegrationService; -import de.tum.cit.aet.artemis.programming.service.gitlabci.GitLabCIResultService; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationGitlabCIGitlabSamlTest; -class GitlabCIServiceTest extends AbstractSpringIntegrationGitlabCIGitlabSamlTest { +class GitlabCIServiceTest extends AbstractProgrammingIntegrationGitlabCIGitlabSamlTest { private static final String TEST_PREFIX = "gitlabciservicetest"; - @Value("${artemis.version-control.url}") - private URL gitlabServerUrl; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; - - @Autowired - private ParticipationTestRepository participationRepository; - - @Autowired - private BuildPlanRepository buildPlanRepository; - - @Autowired - private GitLabCIResultService gitLabCIResultService; - - @Autowired - private BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository; - - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExerciseUtilService exerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - private Long programmingExerciseId; @BeforeEach diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsAuthorizationInterceptorTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsAuthorizationInterceptorTest.java index bd42466d4c6c..00cb27b62167 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsAuthorizationInterceptorTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsAuthorizationInterceptorTest.java @@ -8,13 +8,10 @@ import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; import java.net.URISyntaxException; -import java.net.URL; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRequest; @@ -24,28 +21,17 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.client.MockRestServiceServer; -import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import de.tum.cit.aet.artemis.programming.service.jenkins.JenkinsAuthorizationInterceptor; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationJenkinsGitlabTest; -class JenkinsAuthorizationInterceptorTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class JenkinsAuthorizationInterceptorTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "jenkinsauthintercept"; - @Value("${artemis.continuous-integration.url}") - private URL jenkinsServerUrl; - - @Autowired - JenkinsAuthorizationInterceptor jenkinsAuthorizationInterceptor; - - @Autowired - private RestTemplate restTemplate; - private MockRestServiceServer mockRestServiceServer; /** diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsInternalUriServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsInternalUriServiceTest.java index 404051085bda..7315a73e4b5c 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsInternalUriServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsInternalUriServiceTest.java @@ -14,17 +14,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.util.ReflectionTestUtils; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationJenkinsGitlabTest; import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; -import de.tum.cit.aet.artemis.programming.service.jenkins.JenkinsInternalUrlService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class JenkinsInternalUriServiceTest extends AbstractSpringIntegrationJenkinsGitlabTest { - - @Autowired - private JenkinsInternalUrlService jenkinsInternalUrlService; +class JenkinsInternalUriServiceTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private VcsRepositoryUri vcsRepositoryUri; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsJobPermissionServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsJobPermissionServiceTest.java index c6db962e6c5f..9eb595ca878d 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsJobPermissionServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsJobPermissionServiceTest.java @@ -12,23 +12,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; import org.w3c.dom.DOMException; import org.w3c.dom.Document; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationJenkinsGitlabTest; import de.tum.cit.aet.artemis.programming.service.jenkins.jobs.JenkinsJobPermission; -import de.tum.cit.aet.artemis.programming.service.jenkins.jobs.JenkinsJobPermissionsService; import de.tum.cit.aet.artemis.programming.service.jenkins.jobs.JenkinsJobPermissionsUtils; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class JenkinsJobPermissionServiceTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class JenkinsJobPermissionServiceTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "jenkinsjobpermservice"; - @Autowired - private JenkinsJobPermissionsService jenkinsJobPermissionsService; - private static MockedStatic mockedJenkinsJobPermissionsUtils; @BeforeEach diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsJobServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsJobServiceTest.java index 738312ba6ecd..8c550e21e210 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsJobServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsJobServiceTest.java @@ -21,28 +21,19 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; import org.w3c.dom.Document; import com.offbytwo.jenkins.model.FolderJob; import de.tum.cit.aet.artemis.core.exception.JenkinsException; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationJenkinsGitlabTest; import de.tum.cit.aet.artemis.programming.service.jenkins.JenkinsXmlFileUtils; -import de.tum.cit.aet.artemis.programming.service.jenkins.jobs.JenkinsJobService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class JenkinsJobServiceTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class JenkinsJobServiceTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "jenkinsjobservicetest"; - @Autowired - private JenkinsJobService jenkinsJobService; - - @Autowired - private UserUtilService userUtilService; - private static MockedStatic mockedXmlFileUtils; private Document invalidDocument; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsServiceTest.java index fa7183716ec8..c6e828b48b3a 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/JenkinsServiceTest.java @@ -24,7 +24,6 @@ import org.junit.jupiter.params.provider.EnumSource; import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.StreamUtils; @@ -32,48 +31,17 @@ import com.offbytwo.jenkins.model.JobWithDetails; import de.tum.cit.aet.artemis.core.exception.JenkinsException; -import de.tum.cit.aet.artemis.core.util.CourseUtilService; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; -import de.tum.cit.aet.artemis.programming.ContinuousIntegrationTestService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationJenkinsGitlabTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; import de.tum.cit.aet.artemis.programming.domain.build.BuildPlan; -import de.tum.cit.aet.artemis.programming.repository.BuildPlanRepository; -import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.cit.aet.artemis.programming.service.jenkins.build_plan.JenkinsBuildPlanUtils; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class JenkinsServiceTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class JenkinsServiceTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "jenkinsservicetest"; - @Autowired - private ContinuousIntegrationTestService continuousIntegrationTestService; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; - - @Autowired - private ProgrammingExerciseImportService programmingExerciseImportService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - - @Autowired - private CourseUtilService courseUtilService; - - @Autowired - private BuildPlanRepository buildPlanRepository; - /** * This method initializes the test case by setting up a local repo */ diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseFeedbackCreationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseFeedbackCreationServiceTest.java index a8e26befb1d3..fa9ff3a32070 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseFeedbackCreationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseFeedbackCreationServiceTest.java @@ -10,14 +10,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import de.tum.cit.aet.artemis.assessment.domain.Feedback; import de.tum.cit.aet.artemis.assessment.domain.Visibility; import de.tum.cit.aet.artemis.core.config.Constants; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.exam.domain.ExerciseGroup; -import de.tum.cit.aet.artemis.exam.util.ExamUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationIndependentTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; @@ -27,32 +26,9 @@ import de.tum.cit.aet.artemis.programming.dto.AbstractBuildResultNotificationDTO; import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisIssue; import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisReportDTO; -import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; -class ProgrammingExerciseFeedbackCreationServiceTest extends AbstractSpringIntegrationIndependentTest { - - @Autowired - private ProgrammingExerciseFeedbackCreationService feedbackCreationService; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; - - @Autowired - private ProgrammingExerciseTestCaseTestRepository testCaseRepository; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ExamUtilService examUtilService; +class ProgrammingExerciseFeedbackCreationServiceTest extends AbstractProgrammingIntegrationIndependentTest { private ProgrammingExercise programmingExercise; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/RepositoryAccessServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/RepositoryAccessServiceTest.java index 8d5106407a89..9c64b9e2845e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/service/RepositoryAccessServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/RepositoryAccessServiceTest.java @@ -14,51 +14,24 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; -import de.tum.cit.aet.artemis.core.test_repository.UserTestRepository; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; import de.tum.cit.aet.artemis.core.util.TestConstants; -import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationJenkinsGitlabTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.submissionpolicy.LockRepositoryPolicy; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseFactory; -import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; import de.tum.cit.aet.artemis.programming.web.repository.RepositoryActionType; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class RepositoryAccessServiceTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class RepositoryAccessServiceTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "rastest"; - @Autowired - private UserTestRepository userRepository; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private RepositoryAccessService repositoryAccessService; - - @Autowired - private UserUtilService userUtilService; - - @Autowired - private ProgrammingExerciseUtilService programmingExerciseUtilService; - - @Autowired - private ParticipationUtilService participationUtilService; - - @Autowired - private ProgrammingExerciseGradingService programmingExerciseGradingService; - User student; Course course; diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/connectors/gitlab/GitLabPersonalAccessTokenManagementServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/connectors/gitlab/GitLabPersonalAccessTokenManagementServiceTest.java index e6a8ed1e7eb8..d9f5c658ddb8 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/service/connectors/gitlab/GitLabPersonalAccessTokenManagementServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/connectors/gitlab/GitLabPersonalAccessTokenManagementServiceTest.java @@ -21,7 +21,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.security.test.context.support.WithMockUser; @@ -30,26 +29,14 @@ import org.springframework.web.client.RestTemplate; import de.tum.cit.aet.artemis.core.domain.User; -import de.tum.cit.aet.artemis.core.test_repository.UserTestRepository; -import de.tum.cit.aet.artemis.core.user.util.UserUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationJenkinsGitlabTest; import de.tum.cit.aet.artemis.programming.service.gitlab.GitLabException; -import de.tum.cit.aet.artemis.programming.service.gitlab.GitLabPersonalAccessTokenManagementService; import de.tum.cit.aet.artemis.programming.service.gitlab.dto.GitLabPersonalAccessTokenListResponseDTO; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class GitLabPersonalAccessTokenManagementServiceTest extends AbstractSpringIntegrationJenkinsGitlabTest { +class GitLabPersonalAccessTokenManagementServiceTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private static final String TEST_PREFIX = "gitlabusermanagementservice"; - @Autowired - private GitLabPersonalAccessTokenManagementService gitLabPersonalAccessTokenManagementService; - - @Autowired - private UserTestRepository userRepository; - - @Autowired - private UserUtilService userUtilService; - @BeforeEach void setUp() { gitlabRequestMockProvider.enableMockingOfRequests(); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/connectors/jenkins/build_plan/JenkinsPipelineScriptCreatorTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/connectors/jenkins/build_plan/JenkinsPipelineScriptCreatorTest.java index 81110e30059c..34afe9865fd2 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/service/connectors/jenkins/build_plan/JenkinsPipelineScriptCreatorTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/connectors/jenkins/build_plan/JenkinsPipelineScriptCreatorTest.java @@ -6,36 +6,15 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import de.tum.cit.aet.artemis.core.util.CourseUtilService; +import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationJenkinsGitlabTest; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig; import de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage; import de.tum.cit.aet.artemis.programming.domain.ProjectType; import de.tum.cit.aet.artemis.programming.domain.build.BuildPlan; -import de.tum.cit.aet.artemis.programming.repository.BuildPlanRepository; -import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; -import de.tum.cit.aet.artemis.programming.service.jenkins.build_plan.JenkinsPipelineScriptCreator; -import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository; -import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationJenkinsGitlabTest; -class JenkinsPipelineScriptCreatorTest extends AbstractSpringIntegrationJenkinsGitlabTest { - - @Autowired - private BuildPlanRepository buildPlanRepository; - - @Autowired - private JenkinsPipelineScriptCreator jenkinsPipelineScriptCreator; - - @Autowired - private ProgrammingExerciseTestRepository programmingExerciseRepository; - - @Autowired - private ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository; - - @Autowired - private CourseUtilService courseUtilService; +class JenkinsPipelineScriptCreatorTest extends AbstractProgrammingIntegrationJenkinsGitlabTest { private ProgrammingExercise programmingExercise; diff --git a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleTestArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleTestArchitectureTest.java index 66da61a07636..b88a82887ac7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleTestArchitectureTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleTestArchitectureTest.java @@ -1,26 +1,82 @@ package de.tum.cit.aet.artemis.shared.architecture.module; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noMembers; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; + +import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaField; +import com.tngtech.archunit.core.domain.JavaModifier; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; + import de.tum.cit.aet.artemis.shared.architecture.AbstractArchitectureTest; -public abstract class AbstractModuleTestArchitectureTest extends AbstractArchitectureTest implements ModuleArchitectureTest { +public abstract class AbstractModuleTestArchitectureTest extends AbstractArchitectureTest implements ModuleArchitectureTest { - abstract protected Class getAbstractModuleIntegrationTestClass(); + protected abstract Set> getAbstractModuleIntegrationTestClasses(); @Test void integrationTestsShouldExtendAbstractModuleIntegrationTest() { - classesOfThisModuleThat().haveSimpleNameEndingWith("IntegrationTest").should().beAssignableTo(getAbstractModuleIntegrationTestClass()) - .because("All integration tests should extend %s".formatted(getAbstractModuleIntegrationTestClass())).check(testClasses); + classesOfThisModuleThat().haveSimpleNameEndingWith("IntegrationTest").should().beAssignableTo(isAssignableToAnyAllowedClass(getAbstractModuleIntegrationTestClasses())) + .because("All integration tests should extend any of %s".formatted(getAbstractModuleIntegrationTestClasses())).check(testClasses); } @Test void integrationTestsShouldNotAutowireMembers() { - noMembers().that().areAnnotatedWith(Autowired.class).should().beDeclaredInClassesThat().areAssignableTo(getAbstractModuleIntegrationTestClass()).andShould() - .notBeDeclaredIn(getAbstractModuleIntegrationTestClass()) - .because("Integration tests should not autowire members in any class that inherits from %s".formatted(getAbstractModuleIntegrationTestClass())).check(testClasses); + classes().that().doNotHaveModifier(JavaModifier.ABSTRACT).and().areAssignableTo(isAssignableToAnyAllowedClass(getAbstractModuleIntegrationTestClasses())) + .should(notHaveAutowiredFieldsOrMethods()) + .because("Integration tests should not autowire members in any class that inherits from any of %s".formatted(getAbstractModuleIntegrationTestClasses())) + .check(testClasses); + } + + private static DescribedPredicate isAssignableToAnyAllowedClass(Iterable> allowedClasses) { + return new DescribedPredicate<>(stringifyClasses(allowedClasses)) { + + @Override + public boolean test(JavaClass javaClass) { + for (Class allowedClass : allowedClasses) { + if (javaClass.isAssignableTo(allowedClass)) { + return true; + } + } + return false; + } + }; + } + + private static ArchCondition notHaveAutowiredFieldsOrMethods() { + return new ArchCondition<>("not have @Autowired fields or methods") { + + @Override + public void check(JavaClass javaClass, ConditionEvents events) { + // Check fields for @Autowired + for (JavaField field : javaClass.getFields()) { + if (field.isAnnotatedWith(Autowired.class)) { + String message = String.format("%s has a field %s annotated with @Autowired", javaClass.getName(), field.getName()); + events.add(SimpleConditionEvent.violated(field, message)); + } + } + + // Check methods for @Autowired + javaClass.getMethods().stream().filter(method -> method.isAnnotatedWith(Autowired.class)).forEach(method -> { + String message = String.format("%s has a method %s annotated with @Autowired", javaClass.getName(), method.getName()); + events.add(SimpleConditionEvent.violated(method, message)); + }); + } + }; + } + + private static String stringifyClasses(Iterable> classes) { + StringBuilder stringBuilder = new StringBuilder(); + for (Class clazz : classes) { + stringBuilder.append(clazz.getSimpleName()).append(", "); + } + return stringBuilder.substring(0, stringBuilder.length() - 2); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/tutorialgroup/architecture/TutorialGroupTestArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/tutorialgroup/architecture/TutorialGroupTestArchitectureTest.java index cd7ad9b0ad12..6aea11fdd6fe 100644 --- a/src/test/java/de/tum/cit/aet/artemis/tutorialgroup/architecture/TutorialGroupTestArchitectureTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/tutorialgroup/architecture/TutorialGroupTestArchitectureTest.java @@ -1,9 +1,11 @@ package de.tum.cit.aet.artemis.tutorialgroup.architecture; +import java.util.Set; + import de.tum.cit.aet.artemis.shared.architecture.module.AbstractModuleTestArchitectureTest; import de.tum.cit.aet.artemis.tutorialgroup.AbstractTutorialGroupIntegrationTest; -class TutorialGroupTestArchitectureTest extends AbstractModuleTestArchitectureTest { +class TutorialGroupTestArchitectureTest extends AbstractModuleTestArchitectureTest { @Override public String getModulePackage() { @@ -11,7 +13,7 @@ public String getModulePackage() { } @Override - protected Class getAbstractModuleIntegrationTestClass() { - return AbstractTutorialGroupIntegrationTest.class; + protected Set> getAbstractModuleIntegrationTestClasses() { + return Set.of(AbstractTutorialGroupIntegrationTest.class); } } From 873fd8f655692d6fa9354e0acbd3b3a7b0383bbf Mon Sep 17 00:00:00 2001 From: Marcel Gaupp Date: Tue, 22 Oct 2024 23:36:48 +0200 Subject: [PATCH 03/18] Programming exercises: Add typescript programming language template (#9440) --- build.gradle | 1 + .../programming-exercise-features.inc | 4 + ...ProgrammingPlagiarismDetectionService.java | 4 +- .../domain/ProgrammingLanguage.java | 1 + .../service/TemplateUpgradePolicyService.java | 4 +- .../ci/ContinuousIntegrationService.java | 8 +- ...kinsProgrammingLanguageFeatureService.java | 2 + .../build_plan/JenkinsBuildPlanService.java | 4 +- ...alCIProgrammingLanguageFeatureService.java | 2 + src/main/resources/config/application.yml | 2 + .../templates/aeolus/typescript/default.sh | 33 + .../templates/aeolus/typescript/default.yaml | 16 + .../typescript/regularRuns/pipeline.groovy | 62 + .../templates/typescript/exercise/.gitignore | 132 + .../typescript/exercise/package-lock.json | 53 + .../typescript/exercise/package.json | 19 + .../typescript/exercise/src/bubblesort.ts | 3 + .../typescript/exercise/src/client.ts | 66 + .../typescript/exercise/src/context.ts | 3 + .../typescript/exercise/src/mergesort.ts | 3 + .../typescript/exercise/src/policy.ts | 3 + .../typescript/exercise/src/sortstrategy.ts | 3 + .../typescript/exercise/tsconfig.json | 8 + .../resources/templates/typescript/readme | 86 + .../templates/typescript/solution/.gitignore | 132 + .../typescript/solution/package-lock.json | 53 + .../typescript/solution/package.json | 19 + .../typescript/solution/src/bubblesort.ts | 21 + .../typescript/solution/src/client.ts | 72 + .../typescript/solution/src/comparable.ts | 3 + .../typescript/solution/src/context.ts | 30 + .../typescript/solution/src/mergesort.ts | 69 + .../typescript/solution/src/policy.ts | 28 + .../typescript/solution/src/sortstrategy.ts | 5 + .../typescript/solution/tsconfig.json | 8 + .../templates/typescript/test/.gitignore | 135 + .../templates/typescript/test/jest.config.js | 7 + .../typescript/test/package-lock.json | 4039 +++++++++++++++++ .../templates/typescript/test/package.json | 25 + .../typescript/test/src/behavior.test.ts | 77 + .../typescript/test/src/structural.test.ts | 50 + .../templates/typescript/test/tsconfig.json | 11 + .../programming/programming-exercise.model.ts | 1 + src/test/resources/config/application.yml | 2 + 44 files changed, 5300 insertions(+), 9 deletions(-) create mode 100644 src/main/resources/templates/aeolus/typescript/default.sh create mode 100644 src/main/resources/templates/aeolus/typescript/default.yaml create mode 100644 src/main/resources/templates/jenkins/typescript/regularRuns/pipeline.groovy create mode 100644 src/main/resources/templates/typescript/exercise/.gitignore create mode 100644 src/main/resources/templates/typescript/exercise/package-lock.json create mode 100644 src/main/resources/templates/typescript/exercise/package.json create mode 100644 src/main/resources/templates/typescript/exercise/src/bubblesort.ts create mode 100644 src/main/resources/templates/typescript/exercise/src/client.ts create mode 100644 src/main/resources/templates/typescript/exercise/src/context.ts create mode 100644 src/main/resources/templates/typescript/exercise/src/mergesort.ts create mode 100644 src/main/resources/templates/typescript/exercise/src/policy.ts create mode 100644 src/main/resources/templates/typescript/exercise/src/sortstrategy.ts create mode 100644 src/main/resources/templates/typescript/exercise/tsconfig.json create mode 100644 src/main/resources/templates/typescript/readme create mode 100644 src/main/resources/templates/typescript/solution/.gitignore create mode 100644 src/main/resources/templates/typescript/solution/package-lock.json create mode 100644 src/main/resources/templates/typescript/solution/package.json create mode 100644 src/main/resources/templates/typescript/solution/src/bubblesort.ts create mode 100644 src/main/resources/templates/typescript/solution/src/client.ts create mode 100644 src/main/resources/templates/typescript/solution/src/comparable.ts create mode 100644 src/main/resources/templates/typescript/solution/src/context.ts create mode 100644 src/main/resources/templates/typescript/solution/src/mergesort.ts create mode 100644 src/main/resources/templates/typescript/solution/src/policy.ts create mode 100644 src/main/resources/templates/typescript/solution/src/sortstrategy.ts create mode 100644 src/main/resources/templates/typescript/solution/tsconfig.json create mode 100644 src/main/resources/templates/typescript/test/.gitignore create mode 100644 src/main/resources/templates/typescript/test/jest.config.js create mode 100644 src/main/resources/templates/typescript/test/package-lock.json create mode 100644 src/main/resources/templates/typescript/test/package.json create mode 100644 src/main/resources/templates/typescript/test/src/behavior.test.ts create mode 100644 src/main/resources/templates/typescript/test/src/structural.test.ts create mode 100644 src/main/resources/templates/typescript/test/tsconfig.json diff --git a/build.gradle b/build.gradle index e6f7cd99be1b..30502ab85ec7 100644 --- a/build.gradle +++ b/build.gradle @@ -257,6 +257,7 @@ dependencies { implementation "de.jplag:rust:${jplag_version}" implementation "de.jplag:swift:${jplag_version}" implementation "de.jplag:text:${jplag_version}" + implementation "de.jplag:typescript:${jplag_version}" // those are transitive dependencies of JPlag Text --> Stanford NLP // Note: ideally we would exclude them, but for some reason this does not work diff --git a/docs/user/exercises/programming-exercise-features.inc b/docs/user/exercises/programming-exercise-features.inc index e1ce98df80f1..19eb1e02a680 100644 --- a/docs/user/exercises/programming-exercise-features.inc +++ b/docs/user/exercises/programming-exercise-features.inc @@ -41,6 +41,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------+---------+ | C++ | yes | yes | +----------------------+----------+---------+ + | TypeScript | yes | yes | + +----------------------+----------+---------+ - Not all ``templates`` support the same feature set and supported features can also change depending on the continuous integration system setup. Depending on the feature set, some options might not be available during the creation of the programming exercise. @@ -79,6 +81,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ | C++ | no | no | yes | no | n/a | no | no | L: yes, J: no | +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ + | TypeScript | no | no | yes | no | n/a | no | no | L: yes, J: no | + +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ - *Sequential Test Runs*: ``Artemis`` can generate a build plan which first executes structural and then behavioral tests. This feature can help students to better concentrate on the immediate challenge at hand. - *Static Code Analysis*: ``Artemis`` can generate a build plan which additionally executes static code analysis tools. diff --git a/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java b/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java index 30e568ac85a5..e0707ca62144 100644 --- a/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java @@ -41,6 +41,7 @@ import de.jplag.rlang.RLanguage; import de.jplag.rust.RustLanguage; import de.jplag.swift.SwiftLanguage; +import de.jplag.typescript.TypeScriptLanguage; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.exception.GitException; import de.tum.cit.aet.artemis.core.service.FileService; @@ -321,7 +322,8 @@ private Language getJPlagProgrammingLanguage(ProgrammingExercise programmingExer case R -> new RLanguage(); case RUST -> new RustLanguage(); case SWIFT -> new SwiftLanguage(); - case EMPTY, PHP, DART, HASKELL, ASSEMBLER, OCAML, C_SHARP, SQL, TYPESCRIPT, GO, MATLAB, BASH, VHDL, RUBY, POWERSHELL, ADA -> throw new BadRequestAlertException( + case TYPESCRIPT -> new TypeScriptLanguage(); + case EMPTY, PHP, DART, HASKELL, ASSEMBLER, OCAML, C_SHARP, SQL, GO, MATLAB, BASH, VHDL, RUBY, POWERSHELL, ADA -> throw new BadRequestAlertException( "Programming language " + programmingExercise.getProgrammingLanguage() + " not supported for plagiarism check.", "ProgrammingExercise", "notSupported"); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java index 0fdc216122b9..71f00210c2df 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java @@ -50,6 +50,7 @@ public enum ProgrammingLanguage { R, RUST, SWIFT, + TYPESCRIPT, VHDL, EMPTY ); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java index cc018ae1eb61..07dc44fada0e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java @@ -32,8 +32,8 @@ public TemplateUpgradePolicyService(JavaTemplateUpgradeService javaRepositoryUpg public TemplateUpgradeService getUpgradeService(ProgrammingLanguage programmingLanguage) { return switch (programmingLanguage) { case JAVA -> javaRepositoryUpgradeService; - case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS -> defaultRepositoryUpgradeService; - case C_SHARP, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT -> defaultRepositoryUpgradeService; + case C_SHARP, SQL, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + programmingLanguage); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java index f2f53467d261..ec8e2165c46a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java @@ -219,8 +219,8 @@ enum RepositoryCheckoutPath implements CustomizableCheckoutPath { @Override public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { - case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS -> "assignment"; - case C_SHARP, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT -> "assignment"; + case C_SHARP, SQL, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } @@ -230,9 +230,9 @@ public String forProgrammingLanguage(ProgrammingLanguage language) { @Override public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { - case JAVA, PYTHON, HASKELL, KOTLIN, SWIFT, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS -> ""; + case JAVA, PYTHON, HASKELL, KOTLIN, SWIFT, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT -> ""; case C, VHDL, ASSEMBLER, OCAML -> "tests"; - case C_SHARP, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case C_SHARP, SQL, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java index 8b948e8f0073..9c318267953e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java @@ -11,6 +11,7 @@ import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.R; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.RUST; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.SWIFT; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.TYPESCRIPT; import static de.tum.cit.aet.artemis.programming.domain.ProjectType.FACT; import static de.tum.cit.aet.artemis.programming.domain.ProjectType.GCC; import static de.tum.cit.aet.artemis.programming.domain.ProjectType.GRADLE_GRADLE; @@ -47,5 +48,6 @@ public JenkinsProgrammingLanguageFeatureService() { programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, false)); // Jenkins is not supporting XCODE at the moment programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, true, true, true, false, List.of(PLAIN), false, false)); + programmingLanguageFeatures.put(TYPESCRIPT, new ProgrammingLanguageFeature(TYPESCRIPT, false, false, true, false, false, List.of(), false, false)); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java index dc4ff7a8178a..6dc40e173e4e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java @@ -184,8 +184,8 @@ private JenkinsXmlConfigBuilder builderFor(ProgrammingLanguage programmingLangua throw new UnsupportedOperationException("Xcode templates are not available for Jenkins."); } return switch (programmingLanguage) { - case JAVA, KOTLIN, PYTHON, C, HASKELL, SWIFT, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS -> jenkinsBuildPlanCreator; - case VHDL, ASSEMBLER, OCAML, C_SHARP, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case JAVA, KOTLIN, PYTHON, C, HASKELL, SWIFT, EMPTY, RUST, JAVASCRIPT, R, C_PLUS_PLUS, TYPESCRIPT -> jenkinsBuildPlanCreator; + case VHDL, ASSEMBLER, OCAML, C_SHARP, SQL, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException(programmingLanguage + " templates are not available for Jenkins."); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java index 704755528d3c..d86199310720 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java @@ -14,6 +14,7 @@ import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.R; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.RUST; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.SWIFT; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.TYPESCRIPT; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.VHDL; import static de.tum.cit.aet.artemis.programming.domain.ProjectType.FACT; import static de.tum.cit.aet.artemis.programming.domain.ProjectType.GCC; @@ -54,6 +55,7 @@ public LocalCIProgrammingLanguageFeatureService() { programmingLanguageFeatures.put(R, new ProgrammingLanguageFeature(R, false, false, true, false, false, List.of(), false, true)); programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, true)); programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, false, true, true, false, List.of(PLAIN), false, true)); + programmingLanguageFeatures.put(TYPESCRIPT, new ProgrammingLanguageFeature(TYPESCRIPT, false, false, true, false, false, List.of(), false, true)); programmingLanguageFeatures.put(VHDL, new ProgrammingLanguageFeature(VHDL, false, false, false, false, false, List.of(), false, true)); } } diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 6645696acc2e..85965bb400e0 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -95,6 +95,8 @@ artemis: default: "ghcr.io/ls1intum/artemis-r-docker:v1.0.0" c_plus_plus: default: "ghcr.io/ls1intum/artemis-cpp-docker:v1.0.0" + typescript: + default: "ghcr.io/ls1intum/artemis-javascript-docker:v1.0.0" management: endpoints: diff --git a/src/main/resources/templates/aeolus/typescript/default.sh b/src/main/resources/templates/aeolus/typescript/default.sh new file mode 100644 index 000000000000..6b6dceabd179 --- /dev/null +++ b/src/main/resources/templates/aeolus/typescript/default.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -e +export AEOLUS_INITIAL_DIRECTORY=${PWD} +install_dependencies () { + echo '⚙️ executing install_dependencies' + npm ci --prefer-offline --no-audit +} + +build () { + echo '⚙️ executing build' + npm run build +} + +test () { + echo '⚙️ executing test' + npm run test:ci +} + +main () { + if [[ "${1}" == "aeolus_sourcing" ]]; then + return 0 # just source to use the methods in the subshell, no execution + fi + local _script_name + _script_name=${BASH_SOURCE[0]:-$0} + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; install_dependencies" + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; build" + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; test" +} + +main "${@}" diff --git a/src/main/resources/templates/aeolus/typescript/default.yaml b/src/main/resources/templates/aeolus/typescript/default.yaml new file mode 100644 index 000000000000..de335d090617 --- /dev/null +++ b/src/main/resources/templates/aeolus/typescript/default.yaml @@ -0,0 +1,16 @@ +api: v0.0.1 +metadata: + name: TypeScript + description: Run tests using Jest +actions: + - name: install_dependencies + script: 'npm ci --prefer-offline --no-audit' + - name: build + script: 'npm run build' + - name: test + script: 'npm run test:ci' + runAlways: false + results: + - name: junit + path: 'junit.xml' + type: junit diff --git a/src/main/resources/templates/jenkins/typescript/regularRuns/pipeline.groovy b/src/main/resources/templates/jenkins/typescript/regularRuns/pipeline.groovy new file mode 100644 index 000000000000..1ba259ab3553 --- /dev/null +++ b/src/main/resources/templates/jenkins/typescript/regularRuns/pipeline.groovy @@ -0,0 +1,62 @@ +/* + * This file configures the actual build steps for the automatic grading. + * + * !!! + * For regular exercises, there is no need to make changes to this file. + * Only this base configuration is actively supported by the Artemis maintainers + * and/or your Artemis instance administrators. + * !!! + */ + +dockerImage = '#dockerImage' +dockerFlags = '#dockerArgs' + +/** + * Main function called by Jenkins. + */ +void testRunner() { + docker.image(dockerImage).inside(dockerFlags) { c -> + runTestSteps() + } +} + +private void runTestSteps() { + test() +} + +/** + * Run unit tests + */ +private void test() { + stage('Install Dependencies') { + sh 'npm ci --prefer-offline --no-audit' + } + stage('Build') { + sh 'npm run build' + } + stage('Test') { + sh 'npm run test:ci' + } +} + +/** + * Script of the post build tasks aggregating all JUnit files in $WORKSPACE/results. + * + * Called by Jenkins. + */ +void postBuildTasks() { + sh ''' + rm -rf results + mkdir results + if [ -e junit.xml ] + then + sed -i 's/]*>//g ; s/<\\/testsuites>/<\\/testsuite>/g' junit.xml + fi + cp junit.xml $WORKSPACE/results/ || true + sed -i 's/[^[:print:]\t]/�/g' $WORKSPACE/results/*.xml || true + ''' +} + +// very important, do not remove +// required so that Jenkins finds the methods defined in this script +return this diff --git a/src/main/resources/templates/typescript/exercise/.gitignore b/src/main/resources/templates/typescript/exercise/.gitignore new file mode 100644 index 000000000000..c6ce4cc9ff34 --- /dev/null +++ b/src/main/resources/templates/typescript/exercise/.gitignore @@ -0,0 +1,132 @@ +# NodeJS .gitignore from https://github.com/github/gitignore/blob/main/Node.gitignore + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/src/main/resources/templates/typescript/exercise/package-lock.json b/src/main/resources/templates/typescript/exercise/package-lock.json new file mode 100644 index 000000000000..4c093b19263f --- /dev/null +++ b/src/main/resources/templates/typescript/exercise/package-lock.json @@ -0,0 +1,53 @@ +{ + "name": "artemis-exercise", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "artemis-exercise", + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.15.0", + "typescript": "^5.6.2" + } + }, + "node_modules/@tsconfig/node20": { + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.4.tgz", + "integrity": "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.16.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", + "integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/src/main/resources/templates/typescript/exercise/package.json b/src/main/resources/templates/typescript/exercise/package.json new file mode 100644 index 000000000000..6d49a1f95773 --- /dev/null +++ b/src/main/resources/templates/typescript/exercise/package.json @@ -0,0 +1,19 @@ +{ + "name": "artemis-exercise", + "private": true, + "scripts": { + "build": "tsc", + "start": "node ./dist/client.js" + }, + "exports": { + "./*": { + "types": "./dist/*.d.ts", + "default": "./dist/*.js" + } + }, + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.15.0", + "typescript": "^5.6.2" + } +} diff --git a/src/main/resources/templates/typescript/exercise/src/bubblesort.ts b/src/main/resources/templates/typescript/exercise/src/bubblesort.ts new file mode 100644 index 000000000000..36f8463102ef --- /dev/null +++ b/src/main/resources/templates/typescript/exercise/src/bubblesort.ts @@ -0,0 +1,3 @@ +export default class BubbleSort { + // TODO: implement in performSort(Array) +} diff --git a/src/main/resources/templates/typescript/exercise/src/client.ts b/src/main/resources/templates/typescript/exercise/src/client.ts new file mode 100644 index 000000000000..ad9475b976ad --- /dev/null +++ b/src/main/resources/templates/typescript/exercise/src/client.ts @@ -0,0 +1,66 @@ +const ITERATIONS = 10; +const DATES_LENGTH_MIN = 5; +const DATES_LENGTH_MAX = 15; + +/** + * Main function. + * Add code to demonstrate your implementation here. + */ +function main() { + // TODO: Init Context and Policy + + // Run multiple times to simulate different sorting strategies + for (let i = 0; i < ITERATIONS; i++) { + const dates = createRandomDates(); + + // TODO: Configure context + + console.log('Unsorted Array of dates:'); + console.log(dates); + + // TODO: Sort dates + + console.log('Sorted Array of dates:'); + console.log(dates); + } +} + +/** + * Generates an Array of random Date objects with random Array length between + * {@link DATES_LENGTH_MIN} and {@link DATES_LENGTH_MAX}. + * + * @return an Array of random Date objects + */ +function createRandomDates(): Array { + const length = randomIntegerWithin(DATES_LENGTH_MIN, DATES_LENGTH_MAX); + + const lowestDate = new Date('2024-09-15'); + const highestDate = new Date('2025-01-15'); + + return Array.from(Array(length), () => randomDateWithin(lowestDate, highestDate)); +} + +/** + * Creates a random Date within the given range. + * + * @param low {Date} the lower bound + * @param high {Date} the upper bound + * @return {Date} random Date within the given range + */ +function randomDateWithin(low: Date, high: Date): Date { + const randomTimestamp = randomIntegerWithin(low.valueOf(), high.valueOf()); + return new Date(randomTimestamp); +} + +/** + * Creates a random int within the given range. + * + * @param low {number} the lower bound + * @param high {number} the upper bound + * @returns {number} random int within the given range + */ +function randomIntegerWithin(low: number, high: number): number { + return Math.floor(Math.random() * (high - low + 1)) + low; +} + +main(); diff --git a/src/main/resources/templates/typescript/exercise/src/context.ts b/src/main/resources/templates/typescript/exercise/src/context.ts new file mode 100644 index 000000000000..a667a10bb29e --- /dev/null +++ b/src/main/resources/templates/typescript/exercise/src/context.ts @@ -0,0 +1,3 @@ +export default class Context { + // TODO: Create and implement a Context class according to the UML class diagram +} diff --git a/src/main/resources/templates/typescript/exercise/src/mergesort.ts b/src/main/resources/templates/typescript/exercise/src/mergesort.ts new file mode 100644 index 000000000000..4b07a80b4c31 --- /dev/null +++ b/src/main/resources/templates/typescript/exercise/src/mergesort.ts @@ -0,0 +1,3 @@ +export default class MergeSort { + // TODO: implement in performSort(Array) +} diff --git a/src/main/resources/templates/typescript/exercise/src/policy.ts b/src/main/resources/templates/typescript/exercise/src/policy.ts new file mode 100644 index 000000000000..7c8723feb1a9 --- /dev/null +++ b/src/main/resources/templates/typescript/exercise/src/policy.ts @@ -0,0 +1,3 @@ +export default class Policy { + // TODO: Create and implement a Policy class as described in the problem statement +} diff --git a/src/main/resources/templates/typescript/exercise/src/sortstrategy.ts b/src/main/resources/templates/typescript/exercise/src/sortstrategy.ts new file mode 100644 index 000000000000..40723e61965c --- /dev/null +++ b/src/main/resources/templates/typescript/exercise/src/sortstrategy.ts @@ -0,0 +1,3 @@ +export default interface SortStrategy { + // TODO: Create a SortStrategy interface according to the UML class diagram +} diff --git a/src/main/resources/templates/typescript/exercise/tsconfig.json b/src/main/resources/templates/typescript/exercise/tsconfig.json new file mode 100644 index 000000000000..b26f243b6e4d --- /dev/null +++ b/src/main/resources/templates/typescript/exercise/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "composite": true + } +} diff --git a/src/main/resources/templates/typescript/readme b/src/main/resources/templates/typescript/readme new file mode 100644 index 000000000000..a536c0d80cc9 --- /dev/null +++ b/src/main/resources/templates/typescript/readme @@ -0,0 +1,86 @@ +# Sorting with the Strategy Pattern + +In this exercise, we want to implement sorting algorithms and choose them based on runtime specific variables. + +### Part 1: Sorting + +First, we need to implement two sorting algorithms, in this case `MergeSort` and `BubbleSort`. + +**You have the following tasks:** + +1. [task][Implement Bubble Sort](structural_BubbleSort_has_method,behavior_BubbleSort_should_sort_correctly) +Implement the method `performSort(Array)` in the class `BubbleSort`. Make sure to follow the Bubble Sort algorithm exactly. + +2. [task][Implement Merge Sort](structural_MergeSort_has_method,behavior_MergeSort_should_sort_correctly) +Implement the method `performSort(Array)` in the class `MergeSort`. Make sure to follow the Merge Sort algorithm exactly. + +### Part 2: Strategy Pattern + +We want the application to apply different algorithms for sorting an Array of `Date` objects. +Use the strategy pattern to select the right sorting algorithm at runtime. + +**You have the following tasks:** + +1. SortStrategy Interface +Create a `SortStrategy` interface and adjust the sorting algorithms so that they implement this interface. + +2. [task][Context Class](structural_Context_has_properties,structural_Context_has_methods) +Create and implement a `Context` class following the below class diagram. +Add `get` and `set` accessors for the attribute. + +3. [task][Context Policy](structural_Policy_has_properties,structural_Policy_has_methods) +Create and implement a `Policy` class following the below class diagram. +Add `get` and `set` accessors for the attribute. +`Policy` should implement a simple configuration mechanism: + + 1. [task][Select MergeSort](behavior_Policy_uses_MergeSort_for_big_list) + Select `MergeSort` when the List has more than 10 dates. + + 2. [task][Select BubbleSort](behavior_Policy_uses_BubbleSort_for_small_list) + Select `BubbleSort` when the List has less or equal 10 dates. + +4. Complete the `main()` function which demonstrates switching between two strategies at runtime. + +@startuml + +class Policy { + +Policy(Context) <> + +configure() +} + +class Context { + -dates: Array + +sort() +} + +interface SortStrategy { + +performSort(Array) +} + +class BubbleSort { + +performSort(Array) +} + +class MergeSort { + +performSort(Array) +} + +MergeSort -up-|> SortStrategy #testsColor(structural_MergeSort_has_method) +BubbleSort -up-|> SortStrategy #testsColor(structural_BubbleSort_has_method) +Policy -right-> Context #testsColor(structural_Policy_has_properties): context +Context -right-> SortStrategy #testsColor(structural_Context_has_properties): sortAlgorithm + +hide empty fields +hide empty methods + +@enduml + + +### Part 3: Optional Challenges + +(These are not tested) + +1. Create a new class `QuickSort` that implements `SortStrategy` and implement the Quick Sort algorithm. +2. Make the method `performSort(List)` generic, so that other objects can also be sorted by the same method. + **Hint:** Create a `Comparable` interface. +3. Think about a useful decision in `Policy` when to use the new `QuickSort` algorithm. diff --git a/src/main/resources/templates/typescript/solution/.gitignore b/src/main/resources/templates/typescript/solution/.gitignore new file mode 100644 index 000000000000..c6ce4cc9ff34 --- /dev/null +++ b/src/main/resources/templates/typescript/solution/.gitignore @@ -0,0 +1,132 @@ +# NodeJS .gitignore from https://github.com/github/gitignore/blob/main/Node.gitignore + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/src/main/resources/templates/typescript/solution/package-lock.json b/src/main/resources/templates/typescript/solution/package-lock.json new file mode 100644 index 000000000000..4c093b19263f --- /dev/null +++ b/src/main/resources/templates/typescript/solution/package-lock.json @@ -0,0 +1,53 @@ +{ + "name": "artemis-exercise", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "artemis-exercise", + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.15.0", + "typescript": "^5.6.2" + } + }, + "node_modules/@tsconfig/node20": { + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.4.tgz", + "integrity": "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.16.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", + "integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/src/main/resources/templates/typescript/solution/package.json b/src/main/resources/templates/typescript/solution/package.json new file mode 100644 index 000000000000..6d49a1f95773 --- /dev/null +++ b/src/main/resources/templates/typescript/solution/package.json @@ -0,0 +1,19 @@ +{ + "name": "artemis-exercise", + "private": true, + "scripts": { + "build": "tsc", + "start": "node ./dist/client.js" + }, + "exports": { + "./*": { + "types": "./dist/*.d.ts", + "default": "./dist/*.js" + } + }, + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.15.0", + "typescript": "^5.6.2" + } +} diff --git a/src/main/resources/templates/typescript/solution/src/bubblesort.ts b/src/main/resources/templates/typescript/solution/src/bubblesort.ts new file mode 100644 index 000000000000..f894bfc3fdd9 --- /dev/null +++ b/src/main/resources/templates/typescript/solution/src/bubblesort.ts @@ -0,0 +1,21 @@ +import SortStrategy from './sortstrategy'; +import Comparable from './comparable'; + +export default class BubbleSort implements SortStrategy { + /** + * Sorts objects with BubbleSort. + * + * @param input {Array} the array of objects to be sorted + */ + performSort(input: Array) { + for (let i = input.length - 1; i >= 0; i--) { + for (let j = 0; j < i; j++) { + if (input[j].valueOf() > input[j + 1].valueOf()) { + const temp = input[j]; + input[j] = input[j + 1]; + input[j + 1] = temp; + } + } + } + } +} diff --git a/src/main/resources/templates/typescript/solution/src/client.ts b/src/main/resources/templates/typescript/solution/src/client.ts new file mode 100644 index 000000000000..7c27a4411760 --- /dev/null +++ b/src/main/resources/templates/typescript/solution/src/client.ts @@ -0,0 +1,72 @@ +import Context from './context'; +import Policy from './policy'; + +const ITERATIONS = 10; +const DATES_LENGTH_MIN = 5; +const DATES_LENGTH_MAX = 15; + +/** + * Main function. + * Add code to demonstrate your implementation here. + */ +function main() { + // Init Context and Policy + const context = new Context(); + const policy = new Policy(context); + + // Run multiple times to simulate different sorting strategies + for (let i = 0; i < ITERATIONS; i++) { + const dates = createRandomDates(); + + context.dates = dates; + policy.configure(); + + console.log('Unsorted Array of dates:'); + console.log(dates); + + context.sort(); + + console.log('Sorted Array of dates:'); + console.log(dates); + } +} + +/** + * Generates an Array of random Date objects with random Array length between + * {@link DATES_LENGTH_MIN} and {@link DATES_LENGTH_MAX}. + * + * @return an Array of random Date objects + */ +function createRandomDates(): Array { + const length = randomIntegerWithin(DATES_LENGTH_MIN, DATES_LENGTH_MAX); + + const lowestDate = new Date('2024-09-15'); + const highestDate = new Date('2025-01-15'); + + return Array.from(Array(length), () => randomDateWithin(lowestDate, highestDate)); +} + +/** + * Creates a random Date within the given range. + * + * @param low {Date} the lower bound + * @param high {Date} the upper bound + * @return {Date} random Date within the given range + */ +function randomDateWithin(low: Date, high: Date): Date { + const randomTimestamp = randomIntegerWithin(low.valueOf(), high.valueOf()); + return new Date(randomTimestamp); +} + +/** + * Creates a random int within the given range. + * + * @param low {number} the lower bound + * @param high {number} the upper bound + * @returns {number} random int within the given range + */ +function randomIntegerWithin(low: number, high: number): number { + return Math.floor(Math.random() * (high - low + 1)) + low; +} + +main(); diff --git a/src/main/resources/templates/typescript/solution/src/comparable.ts b/src/main/resources/templates/typescript/solution/src/comparable.ts new file mode 100644 index 000000000000..eade48028180 --- /dev/null +++ b/src/main/resources/templates/typescript/solution/src/comparable.ts @@ -0,0 +1,3 @@ +export default interface Comparable { + valueOf(): number; +} diff --git a/src/main/resources/templates/typescript/solution/src/context.ts b/src/main/resources/templates/typescript/solution/src/context.ts new file mode 100644 index 000000000000..731a630aa235 --- /dev/null +++ b/src/main/resources/templates/typescript/solution/src/context.ts @@ -0,0 +1,30 @@ +import type SortStrategy from './sortstrategy'; + +export default class Context { + private _sortAlgorithm: SortStrategy | null = null; + + private _dates: Array = []; + + /** + * Runs the configured sort algorithm. + */ + sort() { + this._sortAlgorithm?.performSort(this._dates); + } + + get sortAlgorithm(): SortStrategy | null { + return this._sortAlgorithm; + } + + set sortAlgorithm(sortAlgorithm: SortStrategy) { + this._sortAlgorithm = sortAlgorithm; + } + + get dates(): Array { + return this._dates; + } + + set dates(dates: Array) { + this._dates = dates; + } +} diff --git a/src/main/resources/templates/typescript/solution/src/mergesort.ts b/src/main/resources/templates/typescript/solution/src/mergesort.ts new file mode 100644 index 000000000000..383d84a8826c --- /dev/null +++ b/src/main/resources/templates/typescript/solution/src/mergesort.ts @@ -0,0 +1,69 @@ +import SortStrategy from './sortstrategy'; +import Comparable from './comparable'; + +export default class MergeSort implements SortStrategy { + /** + * Wrapper method for the real MergeSort algorithm. + * + * @template T + * @param input {Array} the array of objects to be sorted + */ + performSort(input: Array) { + mergesort(input, 0, input.length - 1); + } +} + +/** + * Recursive merge sort function + * + * @template T + * @param input {Array} + * @param low {number} + * @param high {number} + */ +function mergesort(input: Array, low: number, high: number) { + if (low >= high) { + return; + } + const mid = Math.floor((low + high) / 2); + mergesort(input, low, mid); + mergesort(input, mid + 1, high); + merge(input, low, mid, high); +} + +/** + * Merge function + * + * @template T + * @param input {Array} + * @param low {number} + * @param middle {number} + * @param high {number} + */ +function merge(input: Array, low: number, middle: number, high: number) { + const temp = new Array(high - low + 1); + + let leftIndex = low; + let rightIndex = middle + 1; + let wholeIndex = 0; + + while (leftIndex <= middle && rightIndex <= high) { + if (input[leftIndex].valueOf() <= input[rightIndex].valueOf()) { + temp[wholeIndex] = input[leftIndex++]; + } else { + temp[wholeIndex] = input[rightIndex++]; + } + wholeIndex++; + } + + while (leftIndex <= middle) { + temp[wholeIndex++] = input[leftIndex++]; + } + while (rightIndex <= high) { + temp[wholeIndex++] = input[rightIndex++]; + } + + for (wholeIndex = 0; wholeIndex < temp.length; wholeIndex++) { + input[wholeIndex + low] = temp[wholeIndex]; + } +} diff --git a/src/main/resources/templates/typescript/solution/src/policy.ts b/src/main/resources/templates/typescript/solution/src/policy.ts new file mode 100644 index 000000000000..19bf88b07911 --- /dev/null +++ b/src/main/resources/templates/typescript/solution/src/policy.ts @@ -0,0 +1,28 @@ +import BubbleSort from './bubblesort'; +import MergeSort from './mergesort'; +import Context from './context'; + +const DATES_LENGTH_THRESHOLD = 10; + +export default class Policy { + constructor(private _context: Context) {} + + /** + * Chooses a strategy depending on the number of date objects. + */ + configure() { + if (this._context.dates.length > DATES_LENGTH_THRESHOLD) { + this._context.sortAlgorithm = new MergeSort(); + } else { + this._context.sortAlgorithm = new BubbleSort(); + } + } + + get context(): Context { + return this._context; + } + + set context(context: Context) { + this._context = context; + } +} diff --git a/src/main/resources/templates/typescript/solution/src/sortstrategy.ts b/src/main/resources/templates/typescript/solution/src/sortstrategy.ts new file mode 100644 index 000000000000..f658b5b53705 --- /dev/null +++ b/src/main/resources/templates/typescript/solution/src/sortstrategy.ts @@ -0,0 +1,5 @@ +import Comparable from './comparable'; + +export default interface SortStrategy { + performSort(dates: Array): void; +} diff --git a/src/main/resources/templates/typescript/solution/tsconfig.json b/src/main/resources/templates/typescript/solution/tsconfig.json new file mode 100644 index 000000000000..b26f243b6e4d --- /dev/null +++ b/src/main/resources/templates/typescript/solution/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "composite": true + } +} diff --git a/src/main/resources/templates/typescript/test/.gitignore b/src/main/resources/templates/typescript/test/.gitignore new file mode 100644 index 000000000000..9de920749a26 --- /dev/null +++ b/src/main/resources/templates/typescript/test/.gitignore @@ -0,0 +1,135 @@ +/${studentParentWorkingDirectoryName} +/junit.xml + +# NodeJS .gitignore from https://github.com/github/gitignore/blob/main/Node.gitignore + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/src/main/resources/templates/typescript/test/jest.config.js b/src/main/resources/templates/typescript/test/jest.config.js new file mode 100644 index 000000000000..f5d30d13b959 --- /dev/null +++ b/src/main/resources/templates/typescript/test/jest.config.js @@ -0,0 +1,7 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ +module.exports = { + testEnvironment: "node", + transform: { + "^.+.tsx?$": ["ts-jest",{}], + }, +}; \ No newline at end of file diff --git a/src/main/resources/templates/typescript/test/package-lock.json b/src/main/resources/templates/typescript/test/package-lock.json new file mode 100644 index 000000000000..1db85e84a58f --- /dev/null +++ b/src/main/resources/templates/typescript/test/package-lock.json @@ -0,0 +1,4039 @@ +{ + "name": "artemis-test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "artemis-test", + "workspaces": [ + "${studentParentWorkingDirectoryName}" + ], + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/jest": "^29.5.12", + "jest": "^29.7.0", + "jest-junit": "^16.0.0", + "ts-jest": "^29.2.5", + "typescript": "^5.6.2" + } + }, + "${studentParentWorkingDirectoryName}": { + "name": "artemis-exercise", + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.15.0", + "typescript": "^5.6.2" + } + }, + "${studentParentWorkingDirectoryName}/node_modules/@types/node": { + "version": "20.16.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", + "integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz", + "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.8", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.8", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", + "integrity": "sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.7.tgz", + "integrity": "sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node20": { + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.4.tgz", + "integrity": "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.13", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", + "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/artemis-exercise": { + "resolved": "${studentParentWorkingDirectoryName}", + "link": true + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", + "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-junit": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", + "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/main/resources/templates/typescript/test/package.json b/src/main/resources/templates/typescript/test/package.json new file mode 100644 index 000000000000..d7f50e3d33d3 --- /dev/null +++ b/src/main/resources/templates/typescript/test/package.json @@ -0,0 +1,25 @@ +{ + "name": "artemis-test", + "private": true, + "scripts": { + "build": "tsc -b", + "test": "jest", + "test:ci": "jest --ci --reporters=default --reporters=jest-junit" + }, + "workspaces": [ + "${studentParentWorkingDirectoryName}" + ], + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/jest": "^29.5.12", + "jest": "^29.7.0", + "jest-junit": "^16.0.0", + "ts-jest": "^29.2.5", + "typescript": "^5.6.2" + }, + "jest-junit": { + "classNameTemplate": "{classname}_{title}", + "titleTemplate": "{classname}_{title}", + "ancestorSeparator": "_" + } +} diff --git a/src/main/resources/templates/typescript/test/src/behavior.test.ts b/src/main/resources/templates/typescript/test/src/behavior.test.ts new file mode 100644 index 000000000000..d18092bff9aa --- /dev/null +++ b/src/main/resources/templates/typescript/test/src/behavior.test.ts @@ -0,0 +1,77 @@ +import MergeSort from 'artemis-exercise/mergesort'; +import BubbleSort from 'artemis-exercise/bubblesort'; +import Context from 'artemis-exercise/context'; +import Policy from 'artemis-exercise/policy'; + +// incorrect type structure should fail with runtime errors +const _MergeSort: any = MergeSort; +const _BubbleSort: any = BubbleSort; +const _Context: any = Context; +const _Policy: any = Policy; + +// prettier-ignore +const datesWithCorrectOrder = [ + new Date('2016-02-15'), + new Date('2017-04-15'), + new Date('2017-09-15'), + new Date('2018-11-08'), +]; + +describe('behavior', () => { + let dates: Array; + beforeEach(() => { + // prettier-ignore + dates = [ + new Date('2018-11-08'), + new Date('2017-04-15'), + new Date('2016-02-15'), + new Date('2017-09-15'), + ]; + }); + + describe('BubbleSort', () => { + it('should_sort_correctly', () => { + const bubbleSort = new _BubbleSort(); + bubbleSort.performSort(dates); + expect(dates).toEqual(datesWithCorrectOrder); + }); + }); + + describe('MergeSort', () => { + it('should_sort_correctly', () => { + const mergeSort = new _MergeSort(); + mergeSort.performSort(dates); + expect(dates).toEqual(datesWithCorrectOrder); + }); + }); + + describe('Policy', () => { + it('uses_MergeSort_for_big_list', () => { + const bigList: Array = []; + for (let i = 0; i < 11; i++) { + bigList.push(new Date()); + } + + const context = new _Context(); + context.dates = bigList; + const policy = new _Policy(context); + policy.configure(); + const chosenSortStrategy = context.sortAlgorithm; + expect(chosenSortStrategy).toBeInstanceOf(_MergeSort); + }); + + it('uses_BubbleSort_for_small_list', () => { + const smallList: Array = []; + for (let i = 0; i < 3; i++) { + smallList.push(new Date()); + } + + const context = new _Context(); + context.dates = smallList; + const policy = new _Policy(context); + policy.configure(); + const chosenSortStrategy = context.sortAlgorithm; + expect(chosenSortStrategy).toBeInstanceOf(_BubbleSort); + }); + }); +}); diff --git a/src/main/resources/templates/typescript/test/src/structural.test.ts b/src/main/resources/templates/typescript/test/src/structural.test.ts new file mode 100644 index 000000000000..e6048d0e3f40 --- /dev/null +++ b/src/main/resources/templates/typescript/test/src/structural.test.ts @@ -0,0 +1,50 @@ +import MergeSort from 'artemis-exercise/mergesort'; +import BubbleSort from 'artemis-exercise/bubblesort'; +import Context from 'artemis-exercise/context'; +import Policy from 'artemis-exercise/policy'; + +// incorrect type structure should fail with runtime errors +const _MergeSort: any = MergeSort; +const _BubbleSort: any = BubbleSort; +const _Context: any = Context; +const _Policy: any = Policy; + +describe('structural', () => { + describe('Context', () => { + const context = new _Context(); + + it('has_properties', () => { + expect(context).toHaveProperty('dates'); + expect(context).toHaveProperty('sortAlgorithm'); + }); + + it('has_methods', () => { + expect(context).toHaveProperty('sort', expect.any(Function)); + }); + }); + + describe('Policy', () => { + const context = new _Context(); + const policy = new _Policy(context); + + it('has_properties', () => { + expect(policy).toHaveProperty('context'); + }); + + it('has_methods', () => { + expect(policy).toHaveProperty('configure', expect.any(Function)); + }); + }); + + describe('BubbleSort', () => { + it('has_method', () => { + expect(_BubbleSort.prototype).toHaveProperty('performSort', expect.any(Function)); + }); + }); + + describe('MergeSort', () => { + it('has_method', () => { + expect(_MergeSort.prototype).toHaveProperty('performSort', expect.any(Function)); + }); + }); +}); diff --git a/src/main/resources/templates/typescript/test/tsconfig.json b/src/main/resources/templates/typescript/test/tsconfig.json new file mode 100644 index 000000000000..d7b28c1a1dbf --- /dev/null +++ b/src/main/resources/templates/typescript/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "references": [ + { + "path": "${studentParentWorkingDirectoryName}" + } + ] +} diff --git a/src/main/webapp/app/entities/programming/programming-exercise.model.ts b/src/main/webapp/app/entities/programming/programming-exercise.model.ts index d1d039f7cd38..f00dc636caca 100644 --- a/src/main/webapp/app/entities/programming/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming/programming-exercise.model.ts @@ -26,6 +26,7 @@ export enum ProgrammingLanguage { R = 'R', RUST = 'RUST', SWIFT = 'SWIFT', + TYPESCRIPT = 'TYPESCRIPT', VHDL = 'VHDL', } diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index 16c56a619dd9..484e5f300ba1 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -74,6 +74,8 @@ artemis: default: "~~invalid~~" c_plus_plus: default: "~~invalid~~" + typescript: + default: "~~invalid~~" spring: application: From c17b2c4f505be118f4b2680f158af03dda302ec4 Mon Sep 17 00:00:00 2001 From: Mohamed Bilel Besrour <58034472+BBesrour@users.noreply.github.com> Date: Tue, 22 Oct 2024 23:38:35 +0200 Subject: [PATCH 04/18] Integrated code lifecycle: Improve clean up of temp folders in build agents (#9542) --- .../service/BuildJobExecutionService.java | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java index 9c968c453e47..75bbaf826b00 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java @@ -9,8 +9,11 @@ import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; @@ -27,7 +30,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import com.github.dockerjava.api.command.CreateContainerResponse; @@ -71,6 +77,8 @@ public class BuildJobExecutionService { @Value("${artemis.version-control.default-branch:main}") private String defaultBranch; + private static final Duration TEMP_DIR_RETENTION_PERIOD = Duration.ofMinutes(5); + public BuildJobExecutionService(BuildJobContainerService buildJobContainerService, BuildJobGitService buildJobGitService, BuildAgentDockerService buildAgentDockerService, BuildLogsMap buildLogsMap) { this.buildJobContainerService = buildJobContainerService; @@ -79,6 +87,38 @@ public BuildJobExecutionService(BuildJobContainerService buildJobContainerServic this.buildLogsMap = buildLogsMap; } + /** + * This method is responsible for cleaning up temporary directories that were used for checking out repositories. + * It is triggered when the application is ready and runs asynchronously. + */ + @EventListener(ApplicationReadyEvent.class) + @Async + public void initAsync() { + final ZonedDateTime currentTime = ZonedDateTime.now(); + cleanUpTempDirectoriesAsync(currentTime); + } + + private void cleanUpTempDirectoriesAsync(ZonedDateTime currentTime) { + log.info("Cleaning up temporary directories in {}", CHECKED_OUT_REPOS_TEMP_DIR); + try (DirectoryStream directoryStream = Files.newDirectoryStream(Path.of(CHECKED_OUT_REPOS_TEMP_DIR))) { + for (Path path : directoryStream) { + try { + ZonedDateTime lastModifiedTime = ZonedDateTime.ofInstant(Files.getLastModifiedTime(path).toInstant(), currentTime.getZone()); + if (Files.isDirectory(path) && lastModifiedTime.isBefore(currentTime.minus(TEMP_DIR_RETENTION_PERIOD))) { + FileUtils.deleteDirectory(path.toFile()); + } + } + catch (IOException e) { + log.error("Could not delete temporary directory {}", path, e); + } + } + } + catch (IOException e) { + log.error("Could not delete temporary directories", e); + } + log.info("Clean up of temporary directories in {} completed.", CHECKED_OUT_REPOS_TEMP_DIR); + } + /** * Orchestrates the execution of a build job in a Docker container. This method handles the preparation and configuration of the container, * including cloning the necessary repositories, checking out the appropriate branches, and preparing the environment for the build. @@ -512,15 +552,16 @@ private void deleteCloneRepo(VcsRepositoryUri repositoryUri, @Nullable String co } buildJobGitService.deleteLocalRepository(repository); } + // Do not throw an exception if deletion fails. If an exception occurs, clean up will happen in the next server start. catch (EntityNotFoundException e) { msg = "Error while checking out repository"; buildLogsMap.appendBuildLogEntry(buildJobId, msg); - throw new LocalCIException(msg, e); + log.error("Error while deleting repository with URI {} and Path {}", repositoryUri, repositoryPath, e); } catch (IOException e) { msg = "Error while deleting repository"; buildLogsMap.appendBuildLogEntry(buildJobId, msg); - throw new LocalCIException(msg, e); + log.error("Error while deleting repository with URI {} and Path {}", repositoryUri, repositoryPath, e); } } From dd96df50f711e1bf654198eafa46d94c50c71a3d Mon Sep 17 00:00:00 2001 From: Alexander Joham <73483450+alexjoham@users.noreply.github.com> Date: Wed, 23 Oct 2024 20:57:21 +0200 Subject: [PATCH 05/18] General: Track token usage of LLM service requests (#9455) --- .../artemis/athena/dto/ResponseMetaDTO.java | 17 ++ .../AthenaFeedbackSuggestionsService.java | 54 +++- .../aet/artemis/core/domain/LLMRequest.java | 14 ++ .../artemis/core/domain/LLMServiceType.java | 8 + .../core/domain/LLMTokenUsageRequest.java | 104 ++++++++ .../core/domain/LLMTokenUsageTrace.java | 111 +++++++++ .../LLMTokenUsageRequestRepository.java | 14 ++ .../LLMTokenUsageTraceRepository.java | 14 ++ .../core/service/LLMTokenUsageService.java | 143 +++++++++++ .../iris/dto/IrisChatWebsocketDTO.java | 8 +- .../IrisCompetencyGenerationService.java | 36 ++- .../iris/service/pyris/PyrisJobService.java | 19 +- .../pyris/PyrisStatusUpdateService.java | 38 +-- .../dto/chat/PyrisChatStatusUpdateDTO.java | 3 +- .../PyrisCompetencyStatusUpdateDTO.java | 4 +- .../pyris/dto/data/PyrisLLMCostDTO.java | 4 + .../pyris/job/CompetencyExtractionJob.java | 8 +- .../iris/service/pyris/job/CourseChatJob.java | 7 +- .../service/pyris/job/ExerciseChatJob.java | 7 +- .../job/TrackedSessionBasedPyrisJob.java | 14 ++ .../AbstractIrisChatSessionService.java | 78 +++++- .../session/IrisCourseChatSessionService.java | 38 +-- .../IrisExerciseChatSessionService.java | 37 +-- .../IrisTextExerciseChatSessionService.java | 6 +- .../websocket/IrisChatWebsocketService.java | 10 +- .../changelog/20241018053210_changelog.xml | 49 ++++ .../resources/config/liquibase/master.xml | 1 + .../iris/IrisChatMessageIntegrationTest.java | 2 +- .../IrisChatTokenTrackingIntegrationTest.java | 230 ++++++++++++++++++ .../artemis/iris/IrisChatWebsocketTest.java | 2 +- ...isCompetencyGenerationIntegrationTest.java | 6 +- ...extExerciseChatMessageIntegrationTest.java | 2 +- 32 files changed, 976 insertions(+), 112 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/athena/dto/ResponseMetaDTO.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/domain/LLMRequest.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/domain/LLMServiceType.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/domain/LLMTokenUsageRequest.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/domain/LLMTokenUsageTrace.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/repository/LLMTokenUsageRequestRepository.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/repository/LLMTokenUsageTraceRepository.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/service/LLMTokenUsageService.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/data/PyrisLLMCostDTO.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/TrackedSessionBasedPyrisJob.java create mode 100644 src/main/resources/config/liquibase/changelog/20241018053210_changelog.xml create mode 100644 src/test/java/de/tum/cit/aet/artemis/iris/IrisChatTokenTrackingIntegrationTest.java diff --git a/src/main/java/de/tum/cit/aet/artemis/athena/dto/ResponseMetaDTO.java b/src/main/java/de/tum/cit/aet/artemis/athena/dto/ResponseMetaDTO.java new file mode 100644 index 000000000000..44d36a033552 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/athena/dto/ResponseMetaDTO.java @@ -0,0 +1,17 @@ +package de.tum.cit.aet.artemis.athena.dto; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.core.domain.LLMRequest; + +/** + * DTO representing the meta information in the Athena response. + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record ResponseMetaDTO(TotalUsage totalUsage, List llmRequests) { + + public record TotalUsage(Integer numInputTokens, Integer numOutputTokens, Integer numTotalTokens, Float cost) { + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/athena/service/AthenaFeedbackSuggestionsService.java b/src/main/java/de/tum/cit/aet/artemis/athena/service/AthenaFeedbackSuggestionsService.java index d9c81849b396..210b3c7ba859 100644 --- a/src/main/java/de/tum/cit/aet/artemis/athena/service/AthenaFeedbackSuggestionsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/athena/service/AthenaFeedbackSuggestionsService.java @@ -17,10 +17,18 @@ import de.tum.cit.aet.artemis.athena.dto.ExerciseBaseDTO; import de.tum.cit.aet.artemis.athena.dto.ModelingFeedbackDTO; import de.tum.cit.aet.artemis.athena.dto.ProgrammingFeedbackDTO; +import de.tum.cit.aet.artemis.athena.dto.ResponseMetaDTO; import de.tum.cit.aet.artemis.athena.dto.SubmissionBaseDTO; import de.tum.cit.aet.artemis.athena.dto.TextFeedbackDTO; +import de.tum.cit.aet.artemis.core.domain.LLMRequest; +import de.tum.cit.aet.artemis.core.domain.LLMServiceType; +import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.ConflictException; import de.tum.cit.aet.artemis.core.exception.NetworkingException; +import de.tum.cit.aet.artemis.core.service.LLMTokenUsageService; +import de.tum.cit.aet.artemis.exercise.domain.Exercise; +import de.tum.cit.aet.artemis.exercise.domain.Submission; +import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.modeling.domain.ModelingExercise; import de.tum.cit.aet.artemis.modeling.domain.ModelingSubmission; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; @@ -48,20 +56,24 @@ public class AthenaFeedbackSuggestionsService { private final AthenaDTOConverterService athenaDTOConverterService; + private final LLMTokenUsageService llmTokenUsageService; + /** * Create a new AthenaFeedbackSuggestionsService to receive feedback suggestions from the Athena service. * * @param athenaRestTemplate REST template used for the communication with Athena * @param athenaModuleService Athena module serviced used to determine the urls for different modules - * @param athenaDTOConverterService Service to convert exr + * @param athenaDTOConverterService Service to convert exrcises and submissions to DTOs + * @param llmTokenUsageService Service to store the usage of LLM tokens */ public AthenaFeedbackSuggestionsService(@Qualifier("athenaRestTemplate") RestTemplate athenaRestTemplate, AthenaModuleService athenaModuleService, - AthenaDTOConverterService athenaDTOConverterService) { + AthenaDTOConverterService athenaDTOConverterService, LLMTokenUsageService llmTokenUsageService) { textAthenaConnector = new AthenaConnector<>(athenaRestTemplate, ResponseDTOText.class); programmingAthenaConnector = new AthenaConnector<>(athenaRestTemplate, ResponseDTOProgramming.class); modelingAthenaConnector = new AthenaConnector<>(athenaRestTemplate, ResponseDTOModeling.class); this.athenaDTOConverterService = athenaDTOConverterService; this.athenaModuleService = athenaModuleService; + this.llmTokenUsageService = llmTokenUsageService; } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -69,15 +81,15 @@ private record RequestDTO(ExerciseBaseDTO exercise, SubmissionBaseDTO submission } @JsonInclude(JsonInclude.Include.NON_EMPTY) - private record ResponseDTOText(List data) { + private record ResponseDTOText(List data, ResponseMetaDTO meta) { } @JsonInclude(JsonInclude.Include.NON_EMPTY) - private record ResponseDTOProgramming(List data) { + private record ResponseDTOProgramming(List data, ResponseMetaDTO meta) { } @JsonInclude(JsonInclude.Include.NON_EMPTY) - private record ResponseDTOModeling(List data) { + private record ResponseDTOModeling(List data, ResponseMetaDTO meta) { } /** @@ -100,6 +112,7 @@ public List getTextFeedbackSuggestions(TextExercise exercise, T final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission), isGraded); ResponseDTOText response = textAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0); log.info("Athena responded to '{}' feedback suggestions request: {}", isGraded ? "Graded" : "Non Graded", response.data); + storeTokenUsage(exercise, submission, response.meta, !isGraded); return response.data.stream().toList(); } @@ -117,6 +130,7 @@ public List getProgrammingFeedbackSuggestions(Programmin final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission), isGraded); ResponseDTOProgramming response = programmingAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0); log.info("Athena responded to '{}' feedback suggestions request: {}", isGraded ? "Graded" : "Non Graded", response.data); + storeTokenUsage(exercise, submission, response.meta, !isGraded); return response.data.stream().toList(); } @@ -139,6 +153,36 @@ public List getModelingFeedbackSuggestions(ModelingExercise final RequestDTO request = new RequestDTO(athenaDTOConverterService.ofExercise(exercise), athenaDTOConverterService.ofSubmission(exercise.getId(), submission), isGraded); ResponseDTOModeling response = modelingAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0); log.info("Athena responded to '{}' feedback suggestions request: {}", isGraded ? "Graded" : "Non Graded", response.data); + storeTokenUsage(exercise, submission, response.meta, !isGraded); return response.data; } + + /** + * Store the usage of LLM tokens for a given submission + * + * @param exercise the exercise the submission belongs to + * @param submission the submission for which the tokens were used + * @param meta the meta information of the response from Athena + * @param isPreliminaryFeedback whether the feedback is preliminary or not + */ + private void storeTokenUsage(Exercise exercise, Submission submission, ResponseMetaDTO meta, Boolean isPreliminaryFeedback) { + if (meta == null) { + return; + } + Long courseId = exercise.getCourseViaExerciseGroupOrCourseMember().getId(); + Long userId; + if (submission.getParticipation() instanceof StudentParticipation studentParticipation) { + userId = studentParticipation.getStudent().map(User::getId).orElse(null); + } + else { + userId = null; + } + List llmRequests = meta.llmRequests(); + if (llmRequests == null) { + return; + } + + llmTokenUsageService.saveLLMTokenUsage(llmRequests, LLMServiceType.ATHENA, + (llmTokenUsageBuilder -> llmTokenUsageBuilder.withCourse(courseId).withExercise(exercise.getId()).withUser(userId))); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/LLMRequest.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/LLMRequest.java new file mode 100644 index 000000000000..040b6ad88893 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/LLMRequest.java @@ -0,0 +1,14 @@ +package de.tum.cit.aet.artemis.core.domain; + +/** + * This record is used for the LLMTokenUsageService to provide relevant information about LLM Token usage + * + * @param model LLM model (e.g. gpt-4o) + * @param numInputTokens number of tokens of the LLM call + * @param costPerMillionInputToken cost in Euro per million input tokens + * @param numOutputTokens number of tokens of the LLM answer + * @param costPerMillionOutputToken cost in Euro per million output tokens + * @param pipelineId String with the pipeline name (e.g. IRIS_COURSE_CHAT_PIPELINE) + */ +public record LLMRequest(String model, int numInputTokens, float costPerMillionInputToken, int numOutputTokens, float costPerMillionOutputToken, String pipelineId) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/LLMServiceType.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/LLMServiceType.java new file mode 100644 index 000000000000..22465bc57b5f --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/LLMServiceType.java @@ -0,0 +1,8 @@ +package de.tum.cit.aet.artemis.core.domain; + +/** + * Enum representing different types of LLM (Large Language Model) services used in the system. + */ +public enum LLMServiceType { + IRIS, ATHENA +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/LLMTokenUsageRequest.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/LLMTokenUsageRequest.java new file mode 100644 index 000000000000..81d7ca8f21a8 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/LLMTokenUsageRequest.java @@ -0,0 +1,104 @@ +package de.tum.cit.aet.artemis.core.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Represents the token usage details of a single LLM request, including model, service pipeline, token counts, and costs. + */ +@Entity +@Table(name = "llm_token_usage_request") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class LLMTokenUsageRequest extends DomainObject { + + /** + * LLM model (e.g. gpt-4o) + */ + @Column(name = "model") + private String model; + + /** + * pipeline that was called (e.g. IRIS_COURSE_CHAT_PIPELINE) + */ + @Column(name = "service_pipeline_id") + private String servicePipelineId; + + @Column(name = "num_input_tokens") + private int numInputTokens; + + @Column(name = "cost_per_million_input_tokens") + private float costPerMillionInputTokens; + + @Column(name = "num_output_tokens") + private int numOutputTokens; + + @Column(name = "cost_per_million_output_tokens") + private float costPerMillionOutputTokens; + + @ManyToOne + private LLMTokenUsageTrace trace; + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public String getServicePipelineId() { + return servicePipelineId; + } + + public void setServicePipelineId(String servicePipelineId) { + this.servicePipelineId = servicePipelineId; + } + + public float getCostPerMillionInputTokens() { + return costPerMillionInputTokens; + } + + public void setCostPerMillionInputTokens(float costPerMillionInputToken) { + this.costPerMillionInputTokens = costPerMillionInputToken; + } + + public float getCostPerMillionOutputTokens() { + return costPerMillionOutputTokens; + } + + public void setCostPerMillionOutputTokens(float costPerMillionOutputToken) { + this.costPerMillionOutputTokens = costPerMillionOutputToken; + } + + public int getNumInputTokens() { + return numInputTokens; + } + + public void setNumInputTokens(int numInputTokens) { + this.numInputTokens = numInputTokens; + } + + public int getNumOutputTokens() { + return numOutputTokens; + } + + public void setNumOutputTokens(int numOutputTokens) { + this.numOutputTokens = numOutputTokens; + } + + public LLMTokenUsageTrace getTrace() { + return trace; + } + + public void setTrace(LLMTokenUsageTrace trace) { + this.trace = trace; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/LLMTokenUsageTrace.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/LLMTokenUsageTrace.java new file mode 100644 index 000000000000..1773a0c507da --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/LLMTokenUsageTrace.java @@ -0,0 +1,111 @@ +package de.tum.cit.aet.artemis.core.domain; + +import java.time.ZonedDateTime; +import java.util.HashSet; +import java.util.Set; + +import jakarta.annotation.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * This represents a trace that contains one or more requests of type {@link LLMTokenUsageRequest} + */ +@Entity +@Table(name = "llm_token_usage_trace") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class LLMTokenUsageTrace extends DomainObject { + + @Column(name = "service") + @Enumerated(EnumType.STRING) + private LLMServiceType serviceType; + + @Nullable + @Column(name = "course_id") + private Long courseId; + + @Nullable + @Column(name = "exercise_id") + private Long exerciseId; + + @Column(name = "user_id") + private Long userId; + + @Column(name = "time") + private ZonedDateTime time = ZonedDateTime.now(); + + @Nullable + @Column(name = "iris_message_id") + private Long irisMessageId; + + @OneToMany(mappedBy = "trace", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + private Set llmRequests = new HashSet<>(); + + public LLMServiceType getServiceType() { + return serviceType; + } + + public void setServiceType(LLMServiceType serviceType) { + this.serviceType = serviceType; + } + + public Long getCourseId() { + return courseId; + } + + public void setCourseId(Long courseId) { + this.courseId = courseId; + } + + public Long getExerciseId() { + return exerciseId; + } + + public void setExerciseId(Long exerciseId) { + this.exerciseId = exerciseId; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public ZonedDateTime getTime() { + return time; + } + + public void setTime(ZonedDateTime time) { + this.time = time; + } + + public Set getLLMRequests() { + return llmRequests; + } + + public void setLlmRequests(Set llmRequests) { + this.llmRequests = llmRequests; + } + + public Long getIrisMessageId() { + return irisMessageId; + } + + public void setIrisMessageId(Long messageId) { + this.irisMessageId = messageId; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/LLMTokenUsageRequestRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/LLMTokenUsageRequestRepository.java new file mode 100644 index 000000000000..145383bf124a --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/LLMTokenUsageRequestRepository.java @@ -0,0 +1,14 @@ +package de.tum.cit.aet.artemis.core.repository; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + +import de.tum.cit.aet.artemis.core.domain.LLMTokenUsageRequest; +import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; + +@Profile(PROFILE_CORE) +@Repository +public interface LLMTokenUsageRequestRepository extends ArtemisJpaRepository { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/LLMTokenUsageTraceRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/LLMTokenUsageTraceRepository.java new file mode 100644 index 000000000000..cc1b0e588c4e --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/LLMTokenUsageTraceRepository.java @@ -0,0 +1,14 @@ +package de.tum.cit.aet.artemis.core.repository; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + +import de.tum.cit.aet.artemis.core.domain.LLMTokenUsageTrace; +import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; + +@Profile(PROFILE_CORE) +@Repository +public interface LLMTokenUsageTraceRepository extends ArtemisJpaRepository { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/LLMTokenUsageService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/LLMTokenUsageService.java new file mode 100644 index 000000000000..c3dc2af1e519 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/LLMTokenUsageService.java @@ -0,0 +1,143 @@ +package de.tum.cit.aet.artemis.core.service; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.core.domain.LLMRequest; +import de.tum.cit.aet.artemis.core.domain.LLMServiceType; +import de.tum.cit.aet.artemis.core.domain.LLMTokenUsageRequest; +import de.tum.cit.aet.artemis.core.domain.LLMTokenUsageTrace; +import de.tum.cit.aet.artemis.core.repository.LLMTokenUsageRequestRepository; +import de.tum.cit.aet.artemis.core.repository.LLMTokenUsageTraceRepository; + +/** + * Service for managing the LLMTokenUsage by all LLMs in Artemis + */ +@Profile(PROFILE_CORE) +@Service +public class LLMTokenUsageService { + + private final LLMTokenUsageTraceRepository llmTokenUsageTraceRepository; + + private final LLMTokenUsageRequestRepository llmTokenUsageRequestRepository; + + public LLMTokenUsageService(LLMTokenUsageTraceRepository llmTokenUsageTraceRepository, LLMTokenUsageRequestRepository llmTokenUsageRequestRepository) { + this.llmTokenUsageTraceRepository = llmTokenUsageTraceRepository; + this.llmTokenUsageRequestRepository = llmTokenUsageRequestRepository; + } + + /** + * Saves the token usage to the database. + * This method records the usage of tokens by various LLM services in the system. + * + * @param llmRequests List of LLM requests containing details about the token usage. + * @param serviceType Type of the LLM service (e.g., IRIS, GPT-3). + * @param builderFunction A function that takes an LLMTokenUsageBuilder and returns a modified LLMTokenUsageBuilder. + * This function is used to set additional properties on the LLMTokenUsageTrace object, such as + * the course ID, user ID, exercise ID, and Iris message ID. + * Example usage: + * builder -> builder.withCourse(courseId).withUser(userId) + * @return The saved LLMTokenUsageTrace object, which includes the details of the token usage. + */ + // TODO: this should ideally be done Async + public LLMTokenUsageTrace saveLLMTokenUsage(List llmRequests, LLMServiceType serviceType, Function builderFunction) { + LLMTokenUsageTrace llmTokenUsageTrace = new LLMTokenUsageTrace(); + llmTokenUsageTrace.setServiceType(serviceType); + + LLMTokenUsageBuilder builder = builderFunction.apply(new LLMTokenUsageBuilder()); + builder.getIrisMessageID().ifPresent(llmTokenUsageTrace::setIrisMessageId); + builder.getCourseID().ifPresent(llmTokenUsageTrace::setCourseId); + builder.getExerciseID().ifPresent(llmTokenUsageTrace::setExerciseId); + builder.getUserID().ifPresent(llmTokenUsageTrace::setUserId); + + llmTokenUsageTrace.setLlmRequests(llmRequests.stream().map(LLMTokenUsageService::convertLLMRequestToLLMTokenUsageRequest) + .peek(llmTokenUsageRequest -> llmTokenUsageRequest.setTrace(llmTokenUsageTrace)).collect(Collectors.toSet())); + + return llmTokenUsageTraceRepository.save(llmTokenUsageTrace); + } + + private static LLMTokenUsageRequest convertLLMRequestToLLMTokenUsageRequest(LLMRequest llmRequest) { + LLMTokenUsageRequest llmTokenUsageRequest = new LLMTokenUsageRequest(); + llmTokenUsageRequest.setModel(llmRequest.model()); + llmTokenUsageRequest.setNumInputTokens(llmRequest.numInputTokens()); + llmTokenUsageRequest.setNumOutputTokens(llmRequest.numOutputTokens()); + llmTokenUsageRequest.setCostPerMillionInputTokens(llmRequest.costPerMillionInputToken()); + llmTokenUsageRequest.setCostPerMillionOutputTokens(llmRequest.costPerMillionOutputToken()); + llmTokenUsageRequest.setServicePipelineId(llmRequest.pipelineId()); + return llmTokenUsageRequest; + } + + // TODO: this should ideally be done Async + public void appendRequestsToTrace(List requests, LLMTokenUsageTrace trace) { + var requestSet = requests.stream().map(LLMTokenUsageService::convertLLMRequestToLLMTokenUsageRequest).peek(llmTokenUsageRequest -> llmTokenUsageRequest.setTrace(trace)) + .collect(Collectors.toSet()); + llmTokenUsageRequestRepository.saveAll(requestSet); + } + + /** + * Finds an LLMTokenUsageTrace by its ID. + * + * @param id The ID of the LLMTokenUsageTrace to find. + * @return An Optional containing the LLMTokenUsageTrace if found, or an empty Optional otherwise. + */ + public Optional findLLMTokenUsageTraceById(Long id) { + return llmTokenUsageTraceRepository.findById(id); + } + + /** + * Class LLMTokenUsageBuilder to be used for saveLLMTokenUsage() + */ + public static class LLMTokenUsageBuilder { + + private Optional courseID = Optional.empty(); + + private Optional irisMessageID = Optional.empty(); + + private Optional exerciseID = Optional.empty(); + + private Optional userID = Optional.empty(); + + public LLMTokenUsageBuilder withCourse(Long courseID) { + this.courseID = Optional.ofNullable(courseID); + return this; + } + + public LLMTokenUsageBuilder withIrisMessageID(Long irisMessageID) { + this.irisMessageID = Optional.ofNullable(irisMessageID); + return this; + } + + public LLMTokenUsageBuilder withExercise(Long exerciseID) { + this.exerciseID = Optional.ofNullable(exerciseID); + return this; + } + + public LLMTokenUsageBuilder withUser(Long userID) { + this.userID = Optional.ofNullable(userID); + return this; + } + + public Optional getCourseID() { + return courseID; + } + + public Optional getIrisMessageID() { + return irisMessageID; + } + + public Optional getExerciseID() { + return exerciseID; + } + + public Optional getUserID() { + return userID; + } + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisChatWebsocketDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisChatWebsocketDTO.java index 75b56488e513..9057b8229fb5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisChatWebsocketDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisChatWebsocketDTO.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; +import de.tum.cit.aet.artemis.core.domain.LLMRequest; import de.tum.cit.aet.artemis.iris.domain.message.IrisMessage; import de.tum.cit.aet.artemis.iris.service.IrisRateLimitService; import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageDTO; @@ -21,7 +22,7 @@ */ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record IrisChatWebsocketDTO(IrisWebsocketMessageType type, IrisMessage message, IrisRateLimitService.IrisRateLimitInformation rateLimitInfo, List stages, - List suggestions) { + List suggestions, List tokens) { /** * Creates a new IrisWebsocketDTO instance with the given parameters @@ -31,8 +32,9 @@ public record IrisChatWebsocketDTO(IrisWebsocketMessageType type, IrisMessage me * @param rateLimitInfo the rate limit information * @param stages the stages of the Pyris pipeline */ - public IrisChatWebsocketDTO(@Nullable IrisMessage message, IrisRateLimitService.IrisRateLimitInformation rateLimitInfo, List stages, List suggestions) { - this(determineType(message), message, rateLimitInfo, stages, suggestions); + public IrisChatWebsocketDTO(@Nullable IrisMessage message, IrisRateLimitService.IrisRateLimitInformation rateLimitInfo, List stages, List suggestions, + List tokens) { + this(determineType(message), message, rateLimitInfo, stages, suggestions, tokens); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisCompetencyGenerationService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisCompetencyGenerationService.java index 98182ae92b06..88906ff80628 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisCompetencyGenerationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisCompetencyGenerationService.java @@ -7,7 +7,11 @@ import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyTaxonomy; import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.domain.LLMServiceType; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.repository.CourseRepository; +import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.core.service.LLMTokenUsageService; import de.tum.cit.aet.artemis.iris.service.pyris.PyrisJobService; import de.tum.cit.aet.artemis.iris.service.pyris.PyrisPipelineService; import de.tum.cit.aet.artemis.iris.service.pyris.dto.competency.PyrisCompetencyExtractionPipelineExecutionDTO; @@ -25,14 +29,24 @@ public class IrisCompetencyGenerationService { private final PyrisPipelineService pyrisPipelineService; + private final LLMTokenUsageService llmTokenUsageService; + + private final CourseRepository courseRepository; + private final IrisWebsocketService websocketService; private final PyrisJobService pyrisJobService; - public IrisCompetencyGenerationService(PyrisPipelineService pyrisPipelineService, IrisWebsocketService websocketService, PyrisJobService pyrisJobService) { + private final UserRepository userRepository; + + public IrisCompetencyGenerationService(PyrisPipelineService pyrisPipelineService, LLMTokenUsageService llmTokenUsageService, CourseRepository courseRepository, + IrisWebsocketService websocketService, PyrisJobService pyrisJobService, UserRepository userRepository) { this.pyrisPipelineService = pyrisPipelineService; + this.llmTokenUsageService = llmTokenUsageService; + this.courseRepository = courseRepository; this.websocketService = websocketService; this.pyrisJobService = pyrisJobService; + this.userRepository = userRepository; } /** @@ -48,9 +62,9 @@ public void executeCompetencyExtractionPipeline(User user, Course course, String pyrisPipelineService.executePipeline( "competency-extraction", "default", - pyrisJobService.createTokenForJob(token -> new CompetencyExtractionJob(token, course.getId(), user.getLogin())), + pyrisJobService.createTokenForJob(token -> new CompetencyExtractionJob(token, course.getId(), user.getId())), executionDto -> new PyrisCompetencyExtractionPipelineExecutionDTO(executionDto, courseDescription, currentCompetencies, CompetencyTaxonomy.values(), 5), - stages -> websocketService.send(user.getLogin(), websocketTopic(course.getId()), new PyrisCompetencyStatusUpdateDTO(stages, null)) + stages -> websocketService.send(user.getLogin(), websocketTopic(course.getId()), new PyrisCompetencyStatusUpdateDTO(stages, null, null)) ); // @formatter:on } @@ -58,12 +72,20 @@ public void executeCompetencyExtractionPipeline(User user, Course course, String /** * Takes a status update from Pyris containing a new competency extraction result and sends it to the client via websocket * - * @param userLogin the login of the user - * @param courseId the id of the course + * @param job Job related to the status update * @param statusUpdate the status update containing the new competency recommendations + * @return the same job that was passed in */ - public void handleStatusUpdate(String userLogin, long courseId, PyrisCompetencyStatusUpdateDTO statusUpdate) { - websocketService.send(userLogin, websocketTopic(courseId), statusUpdate); + public CompetencyExtractionJob handleStatusUpdate(CompetencyExtractionJob job, PyrisCompetencyStatusUpdateDTO statusUpdate) { + Course course = courseRepository.findByIdForUpdateElseThrow(job.courseId()); + if (statusUpdate.tokens() != null && !statusUpdate.tokens().isEmpty()) { + llmTokenUsageService.saveLLMTokenUsage(statusUpdate.tokens(), LLMServiceType.IRIS, builder -> builder.withCourse(course.getId()).withUser(job.userId())); + } + + var user = userRepository.findById(job.userId()).orElseThrow(); + websocketService.send(user.getLogin(), websocketTopic(job.courseId()), statusUpdate); + + return job; } private static String websocketTopic(long courseId) { diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisJobService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisJobService.java index 7933e9e20920..16e8969bc463 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisJobService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisJobService.java @@ -78,14 +78,14 @@ public String createTokenForJob(Function tokenToJobFunction) { public String addExerciseChatJob(Long courseId, Long exerciseId, Long sessionId) { var token = generateJobIdToken(); - var job = new ExerciseChatJob(token, courseId, exerciseId, sessionId); + var job = new ExerciseChatJob(token, courseId, exerciseId, sessionId, null); jobMap.put(token, job); return token; } public String addCourseChatJob(Long courseId, Long sessionId) { var token = generateJobIdToken(); - var job = new CourseChatJob(token, courseId, sessionId); + var job = new CourseChatJob(token, courseId, sessionId, null); jobMap.put(token, job); return token; } @@ -107,10 +107,19 @@ public String addIngestionWebhookJob() { /** * Remove a job from the job map. * - * @param token the token + * @param job the job to remove + */ + public void removeJob(PyrisJob job) { + jobMap.remove(job.jobId()); + } + + /** + * Store a job in the job map. + * + * @param job the job to store */ - public void removeJob(String token) { - jobMap.remove(token); + public void updateJob(PyrisJob job) { + jobMap.put(job.jobId(), job); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisStatusUpdateService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisStatusUpdateService.java index 9403da9beb56..cdd398e5c683 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisStatusUpdateService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisStatusUpdateService.java @@ -20,7 +20,9 @@ import de.tum.cit.aet.artemis.iris.service.pyris.job.CourseChatJob; import de.tum.cit.aet.artemis.iris.service.pyris.job.ExerciseChatJob; import de.tum.cit.aet.artemis.iris.service.pyris.job.IngestionWebhookJob; +import de.tum.cit.aet.artemis.iris.service.pyris.job.PyrisJob; import de.tum.cit.aet.artemis.iris.service.pyris.job.TextExerciseChatJob; +import de.tum.cit.aet.artemis.iris.service.pyris.job.TrackedSessionBasedPyrisJob; import de.tum.cit.aet.artemis.iris.service.session.IrisCourseChatSessionService; import de.tum.cit.aet.artemis.iris.service.session.IrisExerciseChatSessionService; import de.tum.cit.aet.artemis.iris.service.session.IrisTextExerciseChatSessionService; @@ -52,15 +54,16 @@ public PyrisStatusUpdateService(PyrisJobService pyrisJobService, IrisExerciseCha } /** - * Handles the status update of a exercise chat job and forwards it to {@link IrisExerciseChatSessionService#handleStatusUpdate(ExerciseChatJob, PyrisChatStatusUpdateDTO)} + * Handles the status update of a exercise chat job and forwards it to + * {@link IrisExerciseChatSessionService#handleStatusUpdate(TrackedSessionBasedPyrisJob, PyrisChatStatusUpdateDTO)} * * @param job the job that is updated * @param statusUpdate the status update */ public void handleStatusUpdate(ExerciseChatJob job, PyrisChatStatusUpdateDTO statusUpdate) { - irisExerciseChatSessionService.handleStatusUpdate(job, statusUpdate); + var updatedJob = irisExerciseChatSessionService.handleStatusUpdate(job, statusUpdate); - removeJobIfTerminated(statusUpdate.stages(), job.jobId()); + removeJobIfTerminatedElseUpdate(statusUpdate.stages(), updatedJob); } /** @@ -71,52 +74,55 @@ public void handleStatusUpdate(ExerciseChatJob job, PyrisChatStatusUpdateDTO sta * @param statusUpdate the status update */ public void handleStatusUpdate(TextExerciseChatJob job, PyrisTextExerciseChatStatusUpdateDTO statusUpdate) { - irisTextExerciseChatSessionService.handleStatusUpdate(job, statusUpdate); + var updatedJob = irisTextExerciseChatSessionService.handleStatusUpdate(job, statusUpdate); - removeJobIfTerminated(statusUpdate.stages(), job.jobId()); + removeJobIfTerminatedElseUpdate(statusUpdate.stages(), updatedJob); } /** * Handles the status update of a course chat job and forwards it to - * {@link de.tum.cit.aet.artemis.iris.service.session.IrisCourseChatSessionService#handleStatusUpdate(CourseChatJob, PyrisChatStatusUpdateDTO)} + * {@link de.tum.cit.aet.artemis.iris.service.session.IrisCourseChatSessionService#handleStatusUpdate(TrackedSessionBasedPyrisJob, PyrisChatStatusUpdateDTO)} * * @param job the job that is updated * @param statusUpdate the status update */ public void handleStatusUpdate(CourseChatJob job, PyrisChatStatusUpdateDTO statusUpdate) { - courseChatSessionService.handleStatusUpdate(job, statusUpdate); + var updatedJob = courseChatSessionService.handleStatusUpdate(job, statusUpdate); - removeJobIfTerminated(statusUpdate.stages(), job.jobId()); + removeJobIfTerminatedElseUpdate(statusUpdate.stages(), updatedJob); } /** * Handles the status update of a competency extraction job and forwards it to - * {@link IrisCompetencyGenerationService#handleStatusUpdate(String, long, PyrisCompetencyStatusUpdateDTO)} + * {@link IrisCompetencyGenerationService#handleStatusUpdate(CompetencyExtractionJob, PyrisCompetencyStatusUpdateDTO)} * * @param job the job that is updated * @param statusUpdate the status update */ public void handleStatusUpdate(CompetencyExtractionJob job, PyrisCompetencyStatusUpdateDTO statusUpdate) { - competencyGenerationService.handleStatusUpdate(job.userLogin(), job.courseId(), statusUpdate); + var updatedJob = competencyGenerationService.handleStatusUpdate(job, statusUpdate); - removeJobIfTerminated(statusUpdate.stages(), job.jobId()); + removeJobIfTerminatedElseUpdate(statusUpdate.stages(), updatedJob); } /** - * Removes the job from the job service if the status update indicates that the job is terminated. - * This is the case if all stages are in a terminal state. + * Removes the job from the job service if the status update indicates that the job is terminated; updates it to distribute changes otherwise. + * A job is terminated if all stages are in a terminal state. *

    * * @see PyrisStageState#isTerminal() * * @param stages the stages of the status update - * @param job the job to remove + * @param job the job to remove or to update */ - private void removeJobIfTerminated(List stages, String job) { + private void removeJobIfTerminatedElseUpdate(List stages, PyrisJob job) { var isDone = stages.stream().map(PyrisStageDTO::state).allMatch(PyrisStageState::isTerminal); if (isDone) { pyrisJobService.removeJob(job); } + else { + pyrisJobService.updateJob(job); + } } /** @@ -128,6 +134,6 @@ private void removeJobIfTerminated(List stages, String job) { */ public void handleStatusUpdate(IngestionWebhookJob job, PyrisLectureIngestionStatusUpdateDTO statusUpdate) { statusUpdate.stages().forEach(stage -> log.info(stage.name() + ":" + stage.message())); - removeJobIfTerminated(statusUpdate.stages(), job.jobId()); + removeJobIfTerminatedElseUpdate(statusUpdate.stages(), job); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/chat/PyrisChatStatusUpdateDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/chat/PyrisChatStatusUpdateDTO.java index cbfa0b2d98dd..5a1024c6315b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/chat/PyrisChatStatusUpdateDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/chat/PyrisChatStatusUpdateDTO.java @@ -4,8 +4,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; +import de.tum.cit.aet.artemis.core.domain.LLMRequest; import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageDTO; @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record PyrisChatStatusUpdateDTO(String result, List stages, List suggestions) { +public record PyrisChatStatusUpdateDTO(String result, List stages, List suggestions, List tokens) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/competency/PyrisCompetencyStatusUpdateDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/competency/PyrisCompetencyStatusUpdateDTO.java index 0956a52f26e8..465c8e5edb65 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/competency/PyrisCompetencyStatusUpdateDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/competency/PyrisCompetencyStatusUpdateDTO.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; +import de.tum.cit.aet.artemis.core.domain.LLMRequest; import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageDTO; /** @@ -13,7 +14,8 @@ * * @param stages List of stages of the generation process * @param result List of competencies recommendations that have been generated so far + * @param tokens List of token usages send by Pyris for tracking the token usage and cost */ @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record PyrisCompetencyStatusUpdateDTO(List stages, List result) { +public record PyrisCompetencyStatusUpdateDTO(List stages, List result, List tokens) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/data/PyrisLLMCostDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/data/PyrisLLMCostDTO.java new file mode 100644 index 000000000000..43c000a879ae --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/data/PyrisLLMCostDTO.java @@ -0,0 +1,4 @@ +package de.tum.cit.aet.artemis.iris.service.pyris.dto.data; + +public record PyrisLLMCostDTO(String modelInfo, int numInputTokens, float costPerInputToken, int numOutputTokens, float costPerOutputToken, String pipeline) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/CompetencyExtractionJob.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/CompetencyExtractionJob.java index 26ab6427a020..b50d8e70b8c9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/CompetencyExtractionJob.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/CompetencyExtractionJob.java @@ -7,12 +7,12 @@ /** * A pyris job that extracts competencies from a course description. * - * @param jobId the job id - * @param courseId the course in which the competencies are being extracted - * @param userLogin the user login of the user who started the job + * @param jobId the job id + * @param courseId the course in which the competencies are being extracted + * @param userId the user who started the job */ @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record CompetencyExtractionJob(String jobId, long courseId, String userLogin) implements PyrisJob { +public record CompetencyExtractionJob(String jobId, long courseId, long userId) implements PyrisJob { @Override public boolean canAccess(Course course) { diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/CourseChatJob.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/CourseChatJob.java index fb4b93a28854..2f389e22ed96 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/CourseChatJob.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/CourseChatJob.java @@ -9,10 +9,15 @@ * This job is used to reference the details of a course chat session when Pyris sends a status update. */ @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record CourseChatJob(String jobId, long courseId, long sessionId) implements PyrisJob { +public record CourseChatJob(String jobId, long courseId, long sessionId, Long traceId) implements TrackedSessionBasedPyrisJob { @Override public boolean canAccess(Course course) { return courseId == course.getId(); } + + @Override + public TrackedSessionBasedPyrisJob withTraceId(long traceId) { + return new CourseChatJob(jobId, courseId, sessionId, traceId); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/ExerciseChatJob.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/ExerciseChatJob.java index 302ae274d8e2..f74e7360be82 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/ExerciseChatJob.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/ExerciseChatJob.java @@ -10,7 +10,7 @@ * This job is used to reference the details of a exercise chat session when Pyris sends a status update. */ @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record ExerciseChatJob(String jobId, long courseId, long exerciseId, long sessionId) implements PyrisJob { +public record ExerciseChatJob(String jobId, long courseId, long exerciseId, long sessionId, Long traceId) implements TrackedSessionBasedPyrisJob { @Override public boolean canAccess(Course course) { @@ -21,4 +21,9 @@ public boolean canAccess(Course course) { public boolean canAccess(Exercise exercise) { return exercise.getId().equals(exerciseId); } + + @Override + public TrackedSessionBasedPyrisJob withTraceId(long traceId) { + return new ExerciseChatJob(jobId, courseId, exerciseId, sessionId, traceId); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/TrackedSessionBasedPyrisJob.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/TrackedSessionBasedPyrisJob.java new file mode 100644 index 000000000000..bdd180103840 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/job/TrackedSessionBasedPyrisJob.java @@ -0,0 +1,14 @@ +package de.tum.cit.aet.artemis.iris.service.pyris.job; + +/** + * A Pyris job that has a session id and stored its own LLM usage tracing ID. + * This is used for chat jobs where we need to reference the trace ID later after chat suggestions have been generated. + */ +public interface TrackedSessionBasedPyrisJob extends PyrisJob { + + long sessionId(); + + Long traceId(); + + TrackedSessionBasedPyrisJob withTraceId(long traceId); +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/AbstractIrisChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/AbstractIrisChatSessionService.java index f732529aae72..6f0b5a9f411a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/AbstractIrisChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/AbstractIrisChatSessionService.java @@ -1,22 +1,43 @@ package de.tum.cit.aet.artemis.iris.service.session; import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import de.tum.cit.aet.artemis.core.domain.LLMServiceType; +import de.tum.cit.aet.artemis.core.service.LLMTokenUsageService; +import de.tum.cit.aet.artemis.iris.domain.message.IrisMessage; +import de.tum.cit.aet.artemis.iris.domain.message.IrisMessageSender; +import de.tum.cit.aet.artemis.iris.domain.message.IrisTextMessageContent; import de.tum.cit.aet.artemis.iris.domain.session.IrisChatSession; import de.tum.cit.aet.artemis.iris.repository.IrisSessionRepository; +import de.tum.cit.aet.artemis.iris.service.IrisMessageService; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.PyrisChatStatusUpdateDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.job.TrackedSessionBasedPyrisJob; +import de.tum.cit.aet.artemis.iris.service.websocket.IrisChatWebsocketService; public abstract class AbstractIrisChatSessionService implements IrisChatBasedFeatureInterface, IrisRateLimitedFeatureInterface { private final IrisSessionRepository irisSessionRepository; + private final IrisMessageService irisMessageService; + + private final IrisChatWebsocketService irisChatWebsocketService; + + private final LLMTokenUsageService llmTokenUsageService; + private final ObjectMapper objectMapper; - public AbstractIrisChatSessionService(IrisSessionRepository irisSessionRepository, ObjectMapper objectMapper) { + public AbstractIrisChatSessionService(IrisSessionRepository irisSessionRepository, ObjectMapper objectMapper, IrisMessageService irisMessageService, + IrisChatWebsocketService irisChatWebsocketService, LLMTokenUsageService llmTokenUsageService) { this.irisSessionRepository = irisSessionRepository; this.objectMapper = objectMapper; + this.irisMessageService = irisMessageService; + this.irisChatWebsocketService = irisChatWebsocketService; + this.llmTokenUsageService = llmTokenUsageService; } /** @@ -40,4 +61,59 @@ protected void updateLatestSuggestions(S session, List latestSuggestions throw new RuntimeException("Could not update latest suggestions for session " + session.getId(), e); } } + + /** + * Handles the status update of a ExerciseChatJob by sending the result to the student via the Websocket. + * + * @param job The job that was executed + * @param statusUpdate The status update of the job + * @return the same job record or a new job record with the same job id if changes were made + */ + public TrackedSessionBasedPyrisJob handleStatusUpdate(TrackedSessionBasedPyrisJob job, PyrisChatStatusUpdateDTO statusUpdate) { + var session = (S) irisSessionRepository.findByIdWithMessagesAndContents(job.sessionId()); + IrisMessage savedMessage; + if (statusUpdate.result() != null) { + var message = new IrisMessage(); + message.addContent(new IrisTextMessageContent(statusUpdate.result())); + savedMessage = irisMessageService.saveMessage(message, session, IrisMessageSender.LLM); + irisChatWebsocketService.sendMessage(session, savedMessage, statusUpdate.stages()); + } + else { + savedMessage = null; + irisChatWebsocketService.sendStatusUpdate(session, statusUpdate.stages(), statusUpdate.suggestions(), statusUpdate.tokens()); + } + + AtomicReference updatedJob = new AtomicReference<>(job); + if (statusUpdate.tokens() != null && !statusUpdate.tokens().isEmpty()) { + if (savedMessage != null) { + // generated message is first sent and generated trace is saved + var llmTokenUsageTrace = llmTokenUsageService.saveLLMTokenUsage(statusUpdate.tokens(), LLMServiceType.IRIS, builder -> { + builder.withIrisMessageID(savedMessage.getId()).withUser(session.getUser().getId()); + this.setLLMTokenUsageParameters(builder, session); + return builder; + }); + + updatedJob.set(job.withTraceId(llmTokenUsageTrace.getId())); + } + else { + // interaction suggestion is sent and appended to the generated trace if it exists + Optional.ofNullable(job.traceId()).flatMap(llmTokenUsageService::findLLMTokenUsageTraceById) + .ifPresentOrElse(trace -> llmTokenUsageService.appendRequestsToTrace(statusUpdate.tokens(), trace), () -> { + var llmTokenUsage = llmTokenUsageService.saveLLMTokenUsage(statusUpdate.tokens(), LLMServiceType.IRIS, builder -> { + builder.withUser(session.getUser().getId()); + this.setLLMTokenUsageParameters(builder, session); + return builder; + }); + + updatedJob.set(job.withTraceId(llmTokenUsage.getId())); + }); + } + } + + updateLatestSuggestions(session, statusUpdate.suggestions()); + + return updatedJob.get(); + } + + protected abstract void setLLMTokenUsageParameters(LLMTokenUsageService.LLMTokenUsageBuilder builder, S session); } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java index 6dea7a728ca6..d2743c2e71a5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java @@ -19,9 +19,8 @@ import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.security.Role; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; +import de.tum.cit.aet.artemis.core.service.LLMTokenUsageService; import de.tum.cit.aet.artemis.iris.domain.message.IrisMessage; -import de.tum.cit.aet.artemis.iris.domain.message.IrisMessageSender; -import de.tum.cit.aet.artemis.iris.domain.message.IrisTextMessageContent; import de.tum.cit.aet.artemis.iris.domain.session.IrisCourseChatSession; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; import de.tum.cit.aet.artemis.iris.repository.IrisCourseChatSessionRepository; @@ -29,8 +28,6 @@ import de.tum.cit.aet.artemis.iris.service.IrisMessageService; import de.tum.cit.aet.artemis.iris.service.IrisRateLimitService; import de.tum.cit.aet.artemis.iris.service.pyris.PyrisPipelineService; -import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.PyrisChatStatusUpdateDTO; -import de.tum.cit.aet.artemis.iris.service.pyris.job.CourseChatJob; import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; import de.tum.cit.aet.artemis.iris.service.websocket.IrisChatWebsocketService; @@ -41,8 +38,6 @@ @Profile(PROFILE_IRIS) public class IrisCourseChatSessionService extends AbstractIrisChatSessionService { - private final IrisMessageService irisMessageService; - private final IrisSettingsService irisSettingsService; private final IrisChatWebsocketService irisChatWebsocketService; @@ -57,11 +52,11 @@ public class IrisCourseChatSessionService extends AbstractIrisChatSessionService private final PyrisPipelineService pyrisPipelineService; - public IrisCourseChatSessionService(IrisMessageService irisMessageService, IrisSettingsService irisSettingsService, IrisChatWebsocketService irisChatWebsocketService, - AuthorizationCheckService authCheckService, IrisSessionRepository irisSessionRepository, IrisRateLimitService rateLimitService, - IrisCourseChatSessionRepository irisCourseChatSessionRepository, PyrisPipelineService pyrisPipelineService, ObjectMapper objectMapper) { - super(irisSessionRepository, objectMapper); - this.irisMessageService = irisMessageService; + public IrisCourseChatSessionService(IrisMessageService irisMessageService, LLMTokenUsageService llmTokenUsageService, IrisSettingsService irisSettingsService, + IrisChatWebsocketService irisChatWebsocketService, AuthorizationCheckService authCheckService, IrisSessionRepository irisSessionRepository, + IrisRateLimitService rateLimitService, IrisCourseChatSessionRepository irisCourseChatSessionRepository, PyrisPipelineService pyrisPipelineService, + ObjectMapper objectMapper) { + super(irisSessionRepository, objectMapper, irisMessageService, irisChatWebsocketService, llmTokenUsageService); this.irisSettingsService = irisSettingsService; this.irisChatWebsocketService = irisChatWebsocketService; this.authCheckService = authCheckService; @@ -126,24 +121,9 @@ private void requestAndHandleResponse(IrisCourseChatSession session, String vari pyrisPipelineService.executeCourseChatPipeline(variant, chatSession, competencyJol); } - /** - * Handles the status update of a CourseChatJob by sending the result to the student via the Websocket. - * - * @param job The job that was executed - * @param statusUpdate The status update of the job - */ - public void handleStatusUpdate(CourseChatJob job, PyrisChatStatusUpdateDTO statusUpdate) { - var session = (IrisCourseChatSession) irisSessionRepository.findByIdWithMessagesAndContents(job.sessionId()); - if (statusUpdate.result() != null) { - var message = new IrisMessage(); - message.addContent(new IrisTextMessageContent(statusUpdate.result())); - var savedMessage = irisMessageService.saveMessage(message, session, IrisMessageSender.LLM); - irisChatWebsocketService.sendMessage(session, savedMessage, statusUpdate.stages()); - } - else { - irisChatWebsocketService.sendStatusUpdate(session, statusUpdate.stages(), statusUpdate.suggestions()); - } - updateLatestSuggestions(session, statusUpdate.suggestions()); + @Override + protected void setLLMTokenUsageParameters(LLMTokenUsageService.LLMTokenUsageBuilder builder, IrisCourseChatSession session) { + builder.withCourse(session.getCourse().getId()); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java index d520540a2db4..a51f1730e98c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java @@ -15,18 +15,15 @@ import de.tum.cit.aet.artemis.core.exception.ConflictException; import de.tum.cit.aet.artemis.core.security.Role; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; +import de.tum.cit.aet.artemis.core.service.LLMTokenUsageService; import de.tum.cit.aet.artemis.exercise.domain.Submission; import de.tum.cit.aet.artemis.iris.domain.message.IrisMessage; -import de.tum.cit.aet.artemis.iris.domain.message.IrisMessageSender; -import de.tum.cit.aet.artemis.iris.domain.message.IrisTextMessageContent; import de.tum.cit.aet.artemis.iris.domain.session.IrisExerciseChatSession; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; import de.tum.cit.aet.artemis.iris.repository.IrisSessionRepository; import de.tum.cit.aet.artemis.iris.service.IrisMessageService; import de.tum.cit.aet.artemis.iris.service.IrisRateLimitService; import de.tum.cit.aet.artemis.iris.service.pyris.PyrisPipelineService; -import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.PyrisChatStatusUpdateDTO; -import de.tum.cit.aet.artemis.iris.service.pyris.job.ExerciseChatJob; import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; import de.tum.cit.aet.artemis.iris.service.websocket.IrisChatWebsocketService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; @@ -42,8 +39,6 @@ @Profile(PROFILE_IRIS) public class IrisExerciseChatSessionService extends AbstractIrisChatSessionService implements IrisRateLimitedFeatureInterface { - private final IrisMessageService irisMessageService; - private final IrisSettingsService irisSettingsService; private final IrisChatWebsocketService irisChatWebsocketService; @@ -62,13 +57,12 @@ public class IrisExerciseChatSessionService extends AbstractIrisChatSessionServi private final ProgrammingExerciseRepository programmingExerciseRepository; - public IrisExerciseChatSessionService(IrisMessageService irisMessageService, IrisSettingsService irisSettingsService, IrisChatWebsocketService irisChatWebsocketService, - AuthorizationCheckService authCheckService, IrisSessionRepository irisSessionRepository, + public IrisExerciseChatSessionService(IrisMessageService irisMessageService, LLMTokenUsageService llmTokenUsageService, IrisSettingsService irisSettingsService, + IrisChatWebsocketService irisChatWebsocketService, AuthorizationCheckService authCheckService, IrisSessionRepository irisSessionRepository, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ProgrammingSubmissionRepository programmingSubmissionRepository, IrisRateLimitService rateLimitService, PyrisPipelineService pyrisPipelineService, ProgrammingExerciseRepository programmingExerciseRepository, ObjectMapper objectMapper) { - super(irisSessionRepository, objectMapper); - this.irisMessageService = irisMessageService; + super(irisSessionRepository, objectMapper, irisMessageService, irisChatWebsocketService, llmTokenUsageService); this.irisSettingsService = irisSettingsService; this.irisChatWebsocketService = irisChatWebsocketService; this.authCheckService = authCheckService; @@ -158,24 +152,9 @@ private Optional getLatestSubmissionIfExists(ProgrammingE .flatMap(sub -> programmingSubmissionRepository.findWithEagerResultsAndFeedbacksAndBuildLogsById(sub.getId())); } - /** - * Handles the status update of a ExerciseChatJob by sending the result to the student via the Websocket. - * - * @param job The job that was executed - * @param statusUpdate The status update of the job - */ - public void handleStatusUpdate(ExerciseChatJob job, PyrisChatStatusUpdateDTO statusUpdate) { - var session = (IrisExerciseChatSession) irisSessionRepository.findByIdWithMessagesAndContents(job.sessionId()); - if (statusUpdate.result() != null) { - var message = new IrisMessage(); - message.addContent(new IrisTextMessageContent(statusUpdate.result())); - var savedMessage = irisMessageService.saveMessage(message, session, IrisMessageSender.LLM); - irisChatWebsocketService.sendMessage(session, savedMessage, statusUpdate.stages()); - } - else { - irisChatWebsocketService.sendStatusUpdate(session, statusUpdate.stages(), statusUpdate.suggestions()); - } - - updateLatestSuggestions(session, statusUpdate.suggestions()); + @Override + protected void setLLMTokenUsageParameters(LLMTokenUsageService.LLMTokenUsageBuilder builder, IrisExerciseChatSession session) { + var exercise = session.getExercise(); + builder.withCourse(exercise.getCourseViaExerciseGroupOrCourseMember().getId()).withExercise(exercise.getId()); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisTextExerciseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisTextExerciseChatSessionService.java index 4520417aad48..8702db7bdf54 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisTextExerciseChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisTextExerciseChatSessionService.java @@ -115,8 +115,10 @@ public void requestAndHandleResponse(IrisTextExerciseChatSession irisSession) { * * @param job The job that is updated * @param statusUpdate The status update + * @return The same job that was passed in */ - public void handleStatusUpdate(TextExerciseChatJob job, PyrisTextExerciseChatStatusUpdateDTO statusUpdate) { + public TextExerciseChatJob handleStatusUpdate(TextExerciseChatJob job, PyrisTextExerciseChatStatusUpdateDTO statusUpdate) { + // TODO: LLM Token Tracking - or better, make this class a subclass of AbstractIrisChatSessionService var session = (IrisTextExerciseChatSession) irisSessionRepository.findByIdElseThrow(job.sessionId()); if (statusUpdate.result() != null) { var message = session.newMessage(); @@ -127,6 +129,8 @@ public void handleStatusUpdate(TextExerciseChatJob job, PyrisTextExerciseChatSta else { irisChatWebsocketService.sendMessage(session, null, statusUpdate.stages()); } + + return job; } @Override diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/websocket/IrisChatWebsocketService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/websocket/IrisChatWebsocketService.java index 320a3103fe99..d6625dcc6f40 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/websocket/IrisChatWebsocketService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/websocket/IrisChatWebsocketService.java @@ -7,6 +7,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import de.tum.cit.aet.artemis.core.domain.LLMRequest; import de.tum.cit.aet.artemis.iris.domain.message.IrisMessage; import de.tum.cit.aet.artemis.iris.domain.session.IrisChatSession; import de.tum.cit.aet.artemis.iris.dto.IrisChatWebsocketDTO; @@ -41,7 +42,7 @@ public void sendMessage(IrisChatSession session, IrisMessage irisMessage, List

    stages) { - this.sendStatusUpdate(session, stages, null); + this.sendStatusUpdate(session, stages, null, null); } /** @@ -61,12 +62,13 @@ public void sendStatusUpdate(IrisChatSession session, List stages * @param session the session to send the status update to * @param stages the stages to send * @param suggestions the suggestions to send + * @param tokens token usage and cost send by Pyris */ - public void sendStatusUpdate(IrisChatSession session, List stages, List suggestions) { + public void sendStatusUpdate(IrisChatSession session, List stages, List suggestions, List tokens) { var user = session.getUser(); var rateLimitInfo = rateLimitService.getRateLimitInformation(user); var topic = "" + session.getId(); // Todo: add more specific topic - var payload = new IrisChatWebsocketDTO(null, rateLimitInfo, stages, suggestions); + var payload = new IrisChatWebsocketDTO(null, rateLimitInfo, stages, suggestions, tokens); websocketService.send(user.getLogin(), topic, payload); } } diff --git a/src/main/resources/config/liquibase/changelog/20241018053210_changelog.xml b/src/main/resources/config/liquibase/changelog/20241018053210_changelog.xml new file mode 100644 index 000000000000..e514ec8e5f58 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241018053210_changelog.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index d496528a13ec..109eefaa1bbf 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -28,6 +28,7 @@ + diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatMessageIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatMessageIntegrationTest.java index 8cda014838a1..96c047ad7345 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatMessageIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatMessageIntegrationTest.java @@ -446,7 +446,7 @@ public String toString() { private void sendStatus(String jobId, String result, List stages, List suggestions) throws Exception { var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of("Authorization", List.of("Bearer " + jobId)))); - request.postWithoutResponseBody("/api/public/pyris/pipelines/tutor-chat/runs/" + jobId + "/status", new PyrisChatStatusUpdateDTO(result, stages, suggestions), + request.postWithoutResponseBody("/api/public/pyris/pipelines/tutor-chat/runs/" + jobId + "/status", new PyrisChatStatusUpdateDTO(result, stages, suggestions, null), HttpStatus.OK, headers); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatTokenTrackingIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatTokenTrackingIntegrationTest.java new file mode 100644 index 000000000000..adb5b009809f --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatTokenTrackingIntegrationTest.java @@ -0,0 +1,230 @@ +package de.tum.cit.aet.artemis.iris; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.awaitility.Awaitility.await; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jgit.api.errors.GitAPIException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.util.LinkedMultiValueMap; + +import de.tum.cit.aet.artemis.core.connector.IrisRequestMockProvider; +import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.domain.LLMRequest; +import de.tum.cit.aet.artemis.core.domain.LLMServiceType; +import de.tum.cit.aet.artemis.core.domain.LLMTokenUsageRequest; +import de.tum.cit.aet.artemis.core.domain.LLMTokenUsageTrace; +import de.tum.cit.aet.artemis.core.repository.LLMTokenUsageRequestRepository; +import de.tum.cit.aet.artemis.core.repository.LLMTokenUsageTraceRepository; +import de.tum.cit.aet.artemis.core.service.LLMTokenUsageService; +import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; +import de.tum.cit.aet.artemis.iris.domain.message.IrisMessage; +import de.tum.cit.aet.artemis.iris.domain.message.IrisMessageContent; +import de.tum.cit.aet.artemis.iris.domain.message.IrisTextMessageContent; +import de.tum.cit.aet.artemis.iris.domain.session.IrisSession; +import de.tum.cit.aet.artemis.iris.repository.IrisMessageRepository; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.PyrisChatStatusUpdateDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState; +import de.tum.cit.aet.artemis.iris.service.session.IrisExerciseChatSessionService; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; +import de.tum.cit.aet.artemis.programming.domain.ProjectType; +import de.tum.cit.aet.artemis.programming.domain.SolutionProgrammingExerciseParticipation; +import de.tum.cit.aet.artemis.programming.domain.TemplateProgrammingExerciseParticipation; + +class IrisChatTokenTrackingIntegrationTest extends AbstractIrisIntegrationTest { + + private static final String TEST_PREFIX = "irischattokentrackingintegration"; + + @Autowired + private IrisExerciseChatSessionService irisExerciseChatSessionService; + + @Autowired + private IrisMessageRepository irisMessageRepository; + + @Autowired + private LLMTokenUsageService llmTokenUsageService; + + @Autowired + private LLMTokenUsageTraceRepository irisLLMTokenUsageTraceRepository; + + @Autowired + private LLMTokenUsageRequestRepository irisLLMTokenUsageRequestRepository; + + @Autowired + private IrisRequestMockProvider irisRequestMockProvider; + + @Autowired + private ParticipationUtilService participationUtilService; + + private ProgrammingExercise exercise; + + private Course course; + + private AtomicBoolean pipelineDone; + + @BeforeEach + void initTestCase() throws GitAPIException, IOException, URISyntaxException { + userUtilService.addUsers(TEST_PREFIX, 2, 0, 0, 0); + course = programmingExerciseUtilService.addCourseWithOneProgrammingExercise(); + exercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); + String projectKey = exercise.getProjectKey(); + exercise.setProjectType(ProjectType.PLAIN_GRADLE); + exercise.setTestRepositoryUri(localVCBaseUrl + "/git/" + projectKey + "/" + projectKey.toLowerCase() + "-tests.git"); + programmingExerciseBuildConfigRepository.save(exercise.getBuildConfig()); + programmingExerciseRepository.save(exercise); + exercise = programmingExerciseRepository.findWithAllParticipationsAndBuildConfigById(exercise.getId()).orElseThrow(); + // Set the correct repository URIs for the template and the solution participation. + String templateRepositorySlug = projectKey.toLowerCase() + "-exercise"; + TemplateProgrammingExerciseParticipation templateParticipation = exercise.getTemplateParticipation(); + templateParticipation.setRepositoryUri(localVCBaseUrl + "/git/" + projectKey + "/" + templateRepositorySlug + ".git"); + templateProgrammingExerciseParticipationRepository.save(templateParticipation); + String solutionRepositorySlug = projectKey.toLowerCase() + "-solution"; + SolutionProgrammingExerciseParticipation solutionParticipation = exercise.getSolutionParticipation(); + solutionParticipation.setRepositoryUri(localVCBaseUrl + "/git/" + projectKey + "/" + solutionRepositorySlug + ".git"); + solutionProgrammingExerciseParticipationRepository.save(solutionParticipation); + String assignmentRepositorySlug = projectKey.toLowerCase() + "-" + TEST_PREFIX + "student1"; + // Add a participation for student1. + ProgrammingExerciseStudentParticipation studentParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(exercise, TEST_PREFIX + "student1"); + studentParticipation.setRepositoryUri(String.format(localVCBaseUrl + "/git/%s/%s.git", projectKey, assignmentRepositorySlug)); + studentParticipation.setBranch(defaultBranch); + programmingExerciseStudentParticipationRepository.save(studentParticipation); + // Prepare the repositories. + localVCLocalCITestService.createAndConfigureLocalRepository(projectKey, templateRepositorySlug); + localVCLocalCITestService.createAndConfigureLocalRepository(projectKey, projectKey.toLowerCase() + "-tests"); + localVCLocalCITestService.createAndConfigureLocalRepository(projectKey, solutionRepositorySlug); + localVCLocalCITestService.createAndConfigureLocalRepository(projectKey, assignmentRepositorySlug); + // Check that the repository folders were created in the file system for all base repositories. + localVCLocalCITestService.verifyRepositoryFoldersExist(exercise, localVCBasePath); + activateIrisGlobally(); + activateIrisFor(course); + activateIrisFor(exercise); + // Clean up the database + irisLLMTokenUsageRequestRepository.deleteAll(); + irisLLMTokenUsageTraceRepository.deleteAll(); + pipelineDone = new AtomicBoolean(false); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testTokenTrackingHandledExerciseChat() throws Exception { + var irisSession = irisExerciseChatSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + var messageToSend = createDefaultMockMessage(irisSession); + var tokens = getMockLLMCosts(); + List doneStage = new ArrayList<>(); + doneStage.add(new PyrisStageDTO("DoneTest", 10, PyrisStageState.DONE, "Done")); + irisRequestMockProvider.mockProgrammingExerciseChatResponse(dto -> { + assertThat(dto.settings().authenticationToken()).isNotNull(); + assertThatNoException().isThrownBy(() -> sendStatus(dto.settings().authenticationToken(), "Hello World", doneStage, tokens)); + pipelineDone.set(true); + }); + request.postWithoutResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, HttpStatus.CREATED); + await().until(pipelineDone::get); + List savedTokenUsageTraces = irisLLMTokenUsageTraceRepository.findAll(); + List savedTokenUsageRequests = irisLLMTokenUsageRequestRepository.findAll(); + assertThat(savedTokenUsageTraces).hasSize(1); + assertThat(savedTokenUsageTraces.getFirst().getServiceType()).isEqualTo(LLMServiceType.IRIS); + assertThat(savedTokenUsageTraces.getFirst().getExerciseId()).isEqualTo(exercise.getId()); + assertThat(savedTokenUsageTraces.getFirst().getCourseId()).isEqualTo(course.getId()); + assertThat(savedTokenUsageRequests).hasSize(5); + for (int i = 0; i < savedTokenUsageRequests.size(); i++) { + LLMTokenUsageRequest usage = savedTokenUsageRequests.get(i); + LLMRequest expectedCost = tokens.get(i); + assertThat(usage.getModel()).isEqualTo(expectedCost.model()); + assertThat(usage.getNumInputTokens()).isEqualTo(expectedCost.numInputTokens()); + assertThat(usage.getNumOutputTokens()).isEqualTo(expectedCost.numOutputTokens()); + assertThat(usage.getCostPerMillionInputTokens()).isEqualTo(expectedCost.costPerMillionInputToken()); + assertThat(usage.getCostPerMillionOutputTokens()).isEqualTo(expectedCost.costPerMillionOutputToken()); + assertThat(usage.getServicePipelineId()).isEqualTo(expectedCost.pipelineId()); + } + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testTokenTrackingSavedExerciseChat() { + var irisSession = irisExerciseChatSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + var irisMessage = createDefaultMockMessage(irisSession); + irisMessageRepository.save(irisMessage); + var tokens = getMockLLMCosts(); + LLMTokenUsageTrace tokenUsageTrace = llmTokenUsageService.saveLLMTokenUsage(tokens, LLMServiceType.IRIS, + builder -> builder.withIrisMessageID(irisMessage.getId()).withExercise(exercise.getId()).withUser(irisSession.getUser().getId()).withCourse(course.getId())); + assertThat(tokenUsageTrace.getServiceType()).isEqualTo(LLMServiceType.IRIS); + assertThat(tokenUsageTrace.getIrisMessageId()).isEqualTo(irisMessage.getId()); + assertThat(tokenUsageTrace.getExerciseId()).isEqualTo(exercise.getId()); + assertThat(tokenUsageTrace.getUserId()).isEqualTo(irisSession.getUser().getId()); + assertThat(tokenUsageTrace.getCourseId()).isEqualTo(course.getId()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testTokenTrackingExerciseChatWithPipelineFail() throws Exception { + var irisSession = irisExerciseChatSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + var messageToSend = createDefaultMockMessage(irisSession); + var tokens = getMockLLMCosts(); + List failedStages = new ArrayList<>(); + failedStages.add(new PyrisStageDTO("TestTokenFail", 10, PyrisStageState.ERROR, "Failed running pipeline")); + irisRequestMockProvider.mockProgrammingExerciseChatResponse(dto -> { + assertThat(dto.settings().authenticationToken()).isNotNull(); + assertThatNoException().isThrownBy(() -> sendStatus(dto.settings().authenticationToken(), null, failedStages, tokens)); + pipelineDone.set(true); + }); + request.postWithoutResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend, HttpStatus.CREATED); + await().until(pipelineDone::get); + List savedTokenUsageTraces = irisLLMTokenUsageTraceRepository.findAll(); + List savedTokenUsageRequests = irisLLMTokenUsageRequestRepository.findAll(); + assertThat(savedTokenUsageTraces).hasSize(1); + assertThat(savedTokenUsageTraces.getFirst().getServiceType()).isEqualTo(LLMServiceType.IRIS); + assertThat(savedTokenUsageTraces.getFirst().getExerciseId()).isEqualTo(exercise.getId()); + assertThat(savedTokenUsageTraces.getFirst().getIrisMessageId()).isEqualTo(messageToSend.getId()); + assertThat(savedTokenUsageTraces.getFirst().getCourseId()).isEqualTo(course.getId()); + assertThat(savedTokenUsageRequests).hasSize(5); + for (int i = 0; i < savedTokenUsageRequests.size(); i++) { + LLMTokenUsageRequest usage = savedTokenUsageRequests.get(i); + LLMRequest expectedCost = tokens.get(i); + assertThat(usage.getModel()).isEqualTo(expectedCost.model()); + assertThat(usage.getNumInputTokens()).isEqualTo(expectedCost.numInputTokens()); + assertThat(usage.getNumOutputTokens()).isEqualTo(expectedCost.numOutputTokens()); + assertThat(usage.getCostPerMillionInputTokens()).isEqualTo(expectedCost.costPerMillionInputToken()); + assertThat(usage.getCostPerMillionOutputTokens()).isEqualTo(expectedCost.costPerMillionOutputToken()); + assertThat(usage.getServicePipelineId()).isEqualTo(expectedCost.pipelineId()); + } + } + + private List getMockLLMCosts() { + List costs = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + costs.add(new LLMRequest("test-llm", i * 10 + 5, i * 0.5f, i * 3 + 5, i * 0.12f, "IRIS_CHAT_EXERCISE_MESSAGE")); + } + return costs; + } + + private IrisMessage createDefaultMockMessage(IrisSession irisSession) { + var messageToSend = irisSession.newMessage(); + messageToSend.addContent(createMockTextContent(), createMockTextContent(), createMockTextContent()); + return messageToSend; + } + + private IrisMessageContent createMockTextContent() { + var text = "The happy dog jumped over the lazy dog."; + return new IrisTextMessageContent(text); + } + + private void sendStatus(String jobId, String result, List stages, List tokens) throws Exception { + var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of("Authorization", List.of("Bearer " + jobId)))); + request.postWithoutResponseBody("/api/public/pyris/pipelines/tutor-chat/runs/" + jobId + "/status", new PyrisChatStatusUpdateDTO(result, stages, null, tokens), + HttpStatus.OK, headers); + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatWebsocketTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatWebsocketTest.java index 03845b59efb7..03afd1453235 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatWebsocketTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/IrisChatWebsocketTest.java @@ -53,7 +53,7 @@ void sendMessage() { message.setMessageDifferentiator(101010); irisChatWebsocketService.sendMessage(irisSession, message, List.of()); verify(websocketMessagingService, times(1)).sendMessageToUser(eq(TEST_PREFIX + "student1"), eq("/topic/iris/" + irisSession.getId()), - eq(new IrisChatWebsocketDTO(message, new IrisRateLimitService.IrisRateLimitInformation(0, -1, 0), List.of(), List.of()))); + eq(new IrisChatWebsocketDTO(message, new IrisRateLimitService.IrisRateLimitInformation(0, -1, 0), List.of(), List.of(), List.of()))); } private IrisTextMessageContent createMockContent() { diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/IrisCompetencyGenerationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/IrisCompetencyGenerationIntegrationTest.java index b4fef850f439..7b7279a25053 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/IrisCompetencyGenerationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/IrisCompetencyGenerationIntegrationTest.java @@ -22,6 +22,7 @@ import de.tum.cit.aet.artemis.iris.service.pyris.dto.competency.PyrisCompetencyStatusUpdateDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.status.PyrisStageState; +import de.tum.cit.aet.artemis.iris.service.pyris.job.CompetencyExtractionJob; class IrisCompetencyGenerationIntegrationTest extends AbstractIrisIntegrationTest { @@ -66,7 +67,10 @@ void generateCompetencies_asEditor_shouldSucceed() throws Exception { List stages = List.of(new PyrisStageDTO("Generating Competencies", 10, PyrisStageState.DONE, null)); // In the real system, this would be triggered by Pyris via a REST call to the Artemis server - irisCompetencyGenerationService.handleStatusUpdate(TEST_PREFIX + "editor1", course.getId(), new PyrisCompetencyStatusUpdateDTO(stages, recommendations)); + String jobId = "testJobId"; + String userLogin = TEST_PREFIX + "editor1"; + CompetencyExtractionJob job = new CompetencyExtractionJob(jobId, course.getId(), userUtilService.getUserByLogin(userLogin).getId()); + irisCompetencyGenerationService.handleStatusUpdate(job, new PyrisCompetencyStatusUpdateDTO(stages, recommendations, null)); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(PyrisCompetencyStatusUpdateDTO.class); verify(websocketMessagingService, timeout(200).times(3)).sendMessageToUser(eq(TEST_PREFIX + "editor1"), eq("/topic/iris/competencies/" + course.getId()), diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/IrisTextExerciseChatMessageIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/IrisTextExerciseChatMessageIntegrationTest.java index 7be2d0e8abc9..0366317fd557 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/IrisTextExerciseChatMessageIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/IrisTextExerciseChatMessageIntegrationTest.java @@ -398,7 +398,7 @@ public String toString() { private void sendStatus(String jobId, String result, List stages, List suggestions) throws Exception { var headers = new HttpHeaders(new LinkedMultiValueMap<>(Map.of("Authorization", List.of("Bearer " + jobId)))); - request.postWithoutResponseBody("/api/public/pyris/pipelines/text-exercise-chat/runs/" + jobId + "/status", new PyrisChatStatusUpdateDTO(result, stages, suggestions), + request.postWithoutResponseBody("/api/public/pyris/pipelines/text-exercise-chat/runs/" + jobId + "/status", new PyrisChatStatusUpdateDTO(result, stages, suggestions, null), HttpStatus.OK, headers); } } From cf918b3a31be708c7d0fe98f851426ac59cb2cbb Mon Sep 17 00:00:00 2001 From: Ege Dogu Kaya <117287716+edkaya@users.noreply.github.com> Date: Thu, 24 Oct 2024 07:58:39 +0200 Subject: [PATCH 06/18] General: Add course archive for old courses from previous semesters (#9343) --- .../artemis/core/dto/CourseForArchiveDTO.java | 16 + .../core/repository/CourseRepository.java | 26 ++ .../artemis/core/service/CourseService.java | 12 + .../aet/artemis/core/web/CourseResource.java | 24 ++ .../course/manage/course-for-archive-dto.ts | 7 + .../manage/course-management.service.ts | 17 ++ .../course-archive.component.html | 70 +++++ .../course-archive.component.scss | 17 ++ .../course-archive.component.ts | 151 ++++++++++ .../course-card-header.component.html | 27 ++ .../course-card-header.component.scss | 58 ++++ .../course-card-header.component.ts | 27 ++ .../app/overview/course-card.component.html | 22 +- .../app/overview/course-card.component.ts | 9 +- src/main/webapp/app/overview/course-card.scss | 43 --- .../app/overview/course-overview.component.ts | 35 ++- .../app/overview/courses-routing.module.ts | 10 + .../app/overview/courses.component.html | 9 +- .../webapp/app/overview/courses.module.ts | 11 +- .../shared/layouts/navbar/navbar.component.ts | 1 + src/main/webapp/i18n/de/course.json | 8 + src/main/webapp/i18n/de/global.json | 1 + .../webapp/i18n/de/student-dashboard.json | 6 +- src/main/webapp/i18n/en/course.json | 8 + src/main/webapp/i18n/en/global.json | 1 + .../webapp/i18n/en/student-dashboard.json | 6 +- .../artemis/core/util/CourseTestService.java | 55 ++++ .../CourseGitlabJenkinsIntegrationTest.java | 12 + .../course/course-archive.component.spec.ts | 277 ++++++++++++++++++ .../course/course-overview.component.spec.ts | 2 - .../component/course/course.component.spec.ts | 22 ++ .../overview/course-card.component.spec.ts | 2 + .../guided-tour.integration.spec.ts | 2 + 33 files changed, 904 insertions(+), 90 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java create mode 100644 src/main/webapp/app/course/manage/course-for-archive-dto.ts create mode 100644 src/main/webapp/app/overview/course-archive/course-archive.component.html create mode 100644 src/main/webapp/app/overview/course-archive/course-archive.component.scss create mode 100644 src/main/webapp/app/overview/course-archive/course-archive.component.ts create mode 100644 src/main/webapp/app/overview/course-card-header/course-card-header.component.html create mode 100644 src/main/webapp/app/overview/course-card-header/course-card-header.component.scss create mode 100644 src/main/webapp/app/overview/course-card-header/course-card-header.component.ts create mode 100644 src/test/javascript/spec/component/course/course-archive.component.spec.ts diff --git a/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java new file mode 100644 index 000000000000..c0b003e668bc --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseForArchiveDTO.java @@ -0,0 +1,16 @@ +package de.tum.cit.aet.artemis.core.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * DTO for representing archived courses from previous semesters. + * + * @param id The id of the course + * @param title The title of the course + * @param semester The semester in which the course was offered + * @param color The background color of the course + * @param icon The icon of the course + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record CourseForArchiveDTO(long id, String title, String semester, String color, String icon) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java index ad4c3ab139f5..c67b80c1236f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/CourseRepository.java @@ -542,4 +542,30 @@ SELECT COUNT(c) > 0 """) boolean hasLearningPathsEnabled(@Param("courseId") long courseId); + /** + * Retrieves all courses that the user has access to based on their role + * or if they are an admin. Filters out any courses that do not belong to + * a specific semester (i.e., have a null semester). + * + * @param userId The id of the user whose courses are being retrieved + * @param isAdmin A boolean flag indicating whether the user is an admin + * @param now The current time to check if the course is still active + * @return A set of courses that the user has access to and belong to a specific semester + */ + @Query(""" + SELECT DISTINCT c + FROM Course c + LEFT JOIN UserGroup ug ON ug.group IN ( + c.studentGroupName, + c.teachingAssistantGroupName, + c.editorGroupName, + c.instructorGroupName + ) + WHERE (:isAdmin = TRUE OR ug.userId = :userId) + AND c.semester IS NOT NULL + AND c.endDate IS NOT NULL + AND c.endDate < :now + """) + Set findInactiveCoursesForUserRolesWithNonNullSemester(@Param("userId") long userId, @Param("isAdmin") boolean isAdmin, @Param("now") ZonedDateTime now); + } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java index cba16e58ad7e..97fee43614b7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/CourseService.java @@ -692,6 +692,18 @@ public List getAllCoursesForManagementOverview(boolean onlyActive) { return courseRepository.findAllCoursesByManagementGroupNames(userGroups); } + /** + * Retrieves all inactive courses from non-null semesters that the current user is enrolled in + * for the course archive. + * + * @return A list of courses for the course archive. + */ + public Set getAllCoursesForCourseArchive() { + var user = userRepository.getUserWithGroupsAndAuthorities(); + boolean isAdmin = authCheckService.isAdmin(user); + return courseRepository.findInactiveCoursesForUserRolesWithNonNullSemester(user.getId(), isAdmin, ZonedDateTime.now()); + } + /** * Get the active students for these particular exercise ids * diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java index 0cb3379e4f99..da9757b1837f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java @@ -72,6 +72,7 @@ import de.tum.cit.aet.artemis.core.config.Constants; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.dto.CourseForArchiveDTO; import de.tum.cit.aet.artemis.core.dto.CourseForDashboardDTO; import de.tum.cit.aet.artemis.core.dto.CourseForImportDTO; import de.tum.cit.aet.artemis.core.dto.CourseManagementDetailViewDTO; @@ -555,6 +556,29 @@ public ResponseEntity> getCoursesForManagementOverview(@RequestPara return ResponseEntity.ok(courseService.getAllCoursesForManagementOverview(onlyActive)); } + /** + * GET /courses/for-archive : get all courses for course archive + * + * @return the ResponseEntity with status 200 (OK) and with body containing + * a set of DTOs, which contain the courses with id, title, semester, color, icon + */ + @GetMapping("courses/for-archive") + @EnforceAtLeastStudent + public ResponseEntity> getCoursesForArchive() { + long start = System.nanoTime(); + User user = userRepository.getUserWithGroupsAndAuthorities(); + log.debug("REST request to get all inactive courses from previous semesters user {} has access to", user.getLogin()); + Set courses = courseService.getAllCoursesForCourseArchive(); + log.debug("courseService.getAllCoursesForCourseArchive done"); + + final Set dto = courses.stream() + .map(course -> new CourseForArchiveDTO(course.getId(), course.getTitle(), course.getSemester(), course.getColor(), course.getCourseIcon())) + .collect(Collectors.toSet()); + + log.debug("GET /courses/for-archive took {} for {} courses for user {}", TimeLogUtil.formatDurationFrom(start), courses.size(), user.getLogin()); + return ResponseEntity.ok(dto); + } + /** * GET /courses/{courseId}/for-enrollment : get a course by id if the course allows enrollment and is currently active. * diff --git a/src/main/webapp/app/course/manage/course-for-archive-dto.ts b/src/main/webapp/app/course/manage/course-for-archive-dto.ts new file mode 100644 index 000000000000..9bce2af6232e --- /dev/null +++ b/src/main/webapp/app/course/manage/course-for-archive-dto.ts @@ -0,0 +1,7 @@ +export class CourseForArchiveDTO { + id: number; + title: string; + semester: string; + color: string; + icon: string; +} diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 1db71b31aac3..7aeb99ef0c0b 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -27,6 +27,7 @@ import { ScoresStorageService } from 'app/course/course-scores/scores-storage.se import { CourseStorageService } from 'app/course/manage/course-storage.service'; import { ExerciseType, ScoresPerExerciseType } from 'app/entities/exercise.model'; import { OnlineCourseDtoModel } from 'app/lti/online-course-dto.model'; +import { CourseForArchiveDTO } from './course-for-archive-dto'; export type EntityResponseType = HttpResponse; export type EntityArrayResponseType = HttpResponse; @@ -343,6 +344,13 @@ export class CourseManagementService { ); } + /** + * Find all courses for the archive using a GET request + */ + getCoursesForArchive(): Observable> { + return this.http.get(`${this.resourceUrl}/for-archive`, { observe: 'response' }); + } + /** * returns the exercise details of the courses for the courses' management dashboard * @param onlyActive - if true, only active courses will be considered in the result @@ -703,4 +711,13 @@ export class CourseManagementService { disableCourseOverviewBackground() { this.courseOverviewSubject.next(false); } + + getSemesterCollapseStateFromStorage(storageId: string): boolean { + const storedCollapseState: string | null = localStorage.getItem('semester.collapseState.' + storageId); + return storedCollapseState ? JSON.parse(storedCollapseState) : false; + } + + setSemesterCollapseState(storageId: string, isCollapsed: boolean) { + localStorage.setItem('semester.collapseState.' + storageId, JSON.stringify(isCollapsed)); + } } diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.html b/src/main/webapp/app/overview/course-archive/course-archive.component.html new file mode 100644 index 000000000000..d88bcdab0bba --- /dev/null +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.html @@ -0,0 +1,70 @@ +@if (courses) { +

    +
    +
    +

    + +
    + @if (courses.length) { +
    + + +
    + } +
    +
    +
    + @if (courses.length) { +
    + @for (semester of semesters; track semester; let last = $last; let i = $index) { +
    + + +
    + @if (!semesterCollapsed[semester]) { +
    +
    + @for (course of coursesBySemester[semester] | searchFilter: ['title'] : searchCourseText; track course) { +
    + +
    + } +
    +
    + } + @if (!last) { +
    + } + } +
    + } @else { +
    +

    +
    + } +
    +} diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.scss b/src/main/webapp/app/overview/course-archive/course-archive.component.scss new file mode 100644 index 000000000000..8f5c6148ae13 --- /dev/null +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.scss @@ -0,0 +1,17 @@ +.course-grid { + display: grid; + // cards can shrink to 325px + grid-template-columns: repeat(auto-fill, minmax(325px, 1fr)); + grid-gap: 1rem; + justify-items: center; +} + +.course-card-wrapper { + width: 100%; + max-width: 400px; +} + +.container-fluid { + // ensure that horizontal spacing in container is consistent + --bs-gutter-x: 2rem; +} diff --git a/src/main/webapp/app/overview/course-archive/course-archive.component.ts b/src/main/webapp/app/overview/course-archive/course-archive.component.ts new file mode 100644 index 000000000000..eb88ee898038 --- /dev/null +++ b/src/main/webapp/app/overview/course-archive/course-archive.component.ts @@ -0,0 +1,151 @@ +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { Course } from 'app/entities/course.model'; +import { CourseManagementService } from '../../course/manage/course-management.service'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { AlertService } from 'app/core/util/alert.service'; +import { onError } from 'app/shared/util/global.utils'; +import { Subscription } from 'rxjs'; +import { faAngleDown, faAngleUp, faArrowDown19, faArrowUp19, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; +import { SizeProp } from '@fortawesome/fontawesome-svg-core'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { CourseCardHeaderComponent } from '../course-card-header/course-card-header.component'; +import { CourseForArchiveDTO } from 'app/course/manage/course-for-archive-dto'; +import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; + +@Component({ + selector: 'jhi-course-archive', + templateUrl: './course-archive.component.html', + styleUrls: ['./course-archive.component.scss'], + standalone: true, + imports: [ArtemisSharedModule, CourseCardHeaderComponent, SearchFilterComponent], +}) +export class CourseArchiveComponent implements OnInit, OnDestroy { + private archiveCourseSubscription: Subscription; + private courseService = inject(CourseManagementService); + private alertService = inject(AlertService); + + courses: CourseForArchiveDTO[] = []; + semesters: string[]; + fullFormOfSemesterStrings: { [key: string]: string } = {}; + semesterCollapsed: { [key: string]: boolean } = {}; + coursesBySemester: { [key: string]: Course[] } = {}; + searchCourseText = ''; + isSortAscending = true; + iconSize: SizeProp = 'lg'; + + //Icons + readonly faAngleDown = faAngleDown; + readonly faAngleUp = faAngleUp; + readonly faArrowDown19 = faArrowDown19; + readonly faArrowUp19 = faArrowUp19; + readonly faQuestionCircle = faQuestionCircle; + + ngOnInit(): void { + this.loadArchivedCourses(); + this.courseService.enableCourseOverviewBackground(); + } + + /** + * Loads all courses that the student has been enrolled in from previous semesters + */ + loadArchivedCourses(): void { + this.archiveCourseSubscription = this.courseService.getCoursesForArchive().subscribe({ + next: (res: HttpResponse) => { + if (res.body) { + this.courses = res.body || []; + this.courses = this.sortCoursesByTitle(this.courses); + this.semesters = this.getUniqueSemesterNamesSorted(this.courses); + this.mapCoursesIntoSemesters(); + } + }, + error: (error: HttpErrorResponse) => onError(this.alertService, error), + }); + } + + /** + * maps existing courses to each semester + */ + mapCoursesIntoSemesters(): void { + this.semesters.forEach((semester) => { + this.semesterCollapsed[semester] = false; + this.courseService.setSemesterCollapseState(semester, false); + this.coursesBySemester[semester] = this.courses.filter((course) => course.semester === semester); + this.fullFormOfSemesterStrings[semester] = semester.startsWith('WS') ? 'artemisApp.course.archive.winterSemester' : 'artemisApp.course.archive.summerSemester'; + }); + } + + ngOnDestroy(): void { + this.archiveCourseSubscription.unsubscribe(); + this.courseService.disableCourseOverviewBackground(); + } + + setSearchValue(searchValue: string): void { + this.searchCourseText = searchValue; + if (searchValue !== '') { + this.expandOrCollapseBasedOnSearchValue(); + } else { + this.getCollapseStateForSemesters(); + } + } + + onSort(): void { + if (this.semesters) { + this.semesters.reverse(); + this.isSortAscending = !this.isSortAscending; + } + } + /** + * if the searched text is matched with a course title, expand the accordion, otherwise collapse + */ + expandOrCollapseBasedOnSearchValue(): void { + for (const semester of this.semesters) { + const hasMatchingCourse = this.coursesBySemester[semester].some((course) => course.title?.toLowerCase().includes(this.searchCourseText.toLowerCase())); + this.semesterCollapsed[semester] = !hasMatchingCourse; + } + } + + getCollapseStateForSemesters(): void { + for (const semester of this.semesters) { + this.semesterCollapsed[semester] = this.courseService.getSemesterCollapseStateFromStorage(semester); + } + } + + toggleCollapseState(semester: string): void { + this.semesterCollapsed[semester] = !this.semesterCollapsed[semester]; + this.courseService.setSemesterCollapseState(semester, this.semesterCollapsed[semester]); + } + + isCourseFoundInSemester(semester: string): boolean { + return this.coursesBySemester[semester].some((course) => course.title?.toLowerCase().includes(this.searchCourseText.toLowerCase())); + } + + sortCoursesByTitle(courses: CourseForArchiveDTO[]): CourseForArchiveDTO[] { + return courses.sort((courseA, courseB) => (courseA.title ?? '').localeCompare(courseB.title ?? '')); + } + + getUniqueSemesterNamesSorted(courses: CourseForArchiveDTO[]): string[] { + return ( + courses + .map((course) => course.semester ?? '') + // filter down to unique values + .filter((course, index, courses) => courses.indexOf(course) === index) + .sort((semesterA, semesterB) => { + // Parse years in base 10 by extracting the two digits after the WS or SS prefix + const yearsCompared = parseInt(semesterB.slice(2, 4), 10) - parseInt(semesterA.slice(2, 4), 10); + if (yearsCompared !== 0) { + return yearsCompared; + } + + // If years are the same, sort WS over SS + const prefixA = semesterA.slice(0, 2); + const prefixB = semesterB.slice(0, 2); + + if (prefixA === prefixB) { + return 0; // Both semesters are the same (either both WS or both SS) + } + + return prefixA === 'WS' ? -1 : 1; // WS should be placed above SS + }) + ); + } +} diff --git a/src/main/webapp/app/overview/course-card-header/course-card-header.component.html b/src/main/webapp/app/overview/course-card-header/course-card-header.component.html new file mode 100644 index 000000000000..69b6b496fa1e --- /dev/null +++ b/src/main/webapp/app/overview/course-card-header/course-card-header.component.html @@ -0,0 +1,27 @@ +
    +
    + @if (courseIcon()) { +
    + +
    + } @else { +
    + {{ courseTitle() | slice: 0 : 1 }} +
    + } +
    +
    + {{ courseTitle() }} +
    +
    +
    + +
    +
    +
    diff --git a/src/main/webapp/app/overview/course-card-header/course-card-header.component.scss b/src/main/webapp/app/overview/course-card-header/course-card-header.component.scss new file mode 100644 index 000000000000..ff73cfff5a6d --- /dev/null +++ b/src/main/webapp/app/overview/course-card-header/course-card-header.component.scss @@ -0,0 +1,58 @@ +.card-header { + // needed, otherwise hover effect won't work due to stretched-link class + z-index: 2; + position: relative; + height: 85px; + opacity: 1; + filter: alpha(opacity = 100); + transition: 0.15s; + background-color: var(--background-color-for-hover) !important; + // inner border radius : outer border radius - outer border thickness (8px - 1px) + border-top-left-radius: 7px; + border-top-right-radius: 7px; + + &:hover { + background-color: color-mix(in srgb, var(--background-color-for-hover), transparent 15%) !important; + } + + .container { + height: 80px; + + .row { + height: 80px; + } + } + + .card-title { + overflow: hidden; + padding-bottom: 1px; + // matches 4 lines + max-height: 76px; + } + + .course-circle { + // same size as the course icons + height: 65px; + min-width: 65px; + background-color: var(--course-image-bg); + border-radius: 50%; + display: inline-block; + color: var(--bs-body-color); + } +} + +.container { + max-width: unset; +} + +jhi-secured-image { + ::ng-deep img { + border-radius: 50%; + height: 65px; + width: auto; + } +} + +.card-header-title { + max-width: 280px; +} diff --git a/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts new file mode 100644 index 000000000000..3b1bb9b1c433 --- /dev/null +++ b/src/main/webapp/app/overview/course-card-header/course-card-header.component.ts @@ -0,0 +1,27 @@ +import { Component, OnInit, input } from '@angular/core'; +import { CachingStrategy } from 'app/shared/image/secured-image.component'; +import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; + +@Component({ + selector: 'jhi-course-card-header', + templateUrl: './course-card-header.component.html', + styleUrls: ['./course-card-header.component.scss'], + standalone: true, + imports: [ArtemisSharedModule], +}) +export class CourseCardHeaderComponent implements OnInit { + protected readonly ARTEMIS_DEFAULT_COLOR = ARTEMIS_DEFAULT_COLOR; + courseIcon = input.required(); + courseTitle = input.required(); + courseColor = input.required(); + courseId = input.required(); + archiveMode = input(false); + + CachingStrategy = CachingStrategy; + color: string; + + ngOnInit() { + this.color = this.courseColor() || this.ARTEMIS_DEFAULT_COLOR; + } +} diff --git a/src/main/webapp/app/overview/course-card.component.html b/src/main/webapp/app/overview/course-card.component.html index 4b69401c3d85..51e1df0c089c 100644 --- a/src/main/webapp/app/overview/course-card.component.html +++ b/src/main/webapp/app/overview/course-card.component.html @@ -1,25 +1,5 @@
    -
    -
    - @if (course.courseIcon) { -
    - -
    - } @else { -
    - {{ course.title | slice: 0 : 1 }} -
    - } -
    -
    - {{ course.title }} -
    -
    -
    - -
    -
    -
    +
    diff --git a/src/main/webapp/app/overview/course-card.component.ts b/src/main/webapp/app/overview/course-card.component.ts index 043c4a892b48..ce8bbb0bcc7e 100644 --- a/src/main/webapp/app/overview/course-card.component.ts +++ b/src/main/webapp/app/overview/course-card.component.ts @@ -12,11 +12,18 @@ import { ScoresStorageService } from 'app/course/course-scores/scores-storage.se import { ScoreType } from 'app/shared/constants/score-type.constants'; import { CourseScores } from 'app/course/course-scores/course-scores'; import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; +import { CourseCardHeaderComponent } from './course-card-header/course-card-header.component'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { NgxChartsModule, PieChartModule } from '@swimlane/ngx-charts'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { RouterLink } from '@angular/router'; @Component({ selector: 'jhi-overview-course-card', templateUrl: './course-card.component.html', styleUrls: ['course-card.scss'], + standalone: true, + imports: [CourseCardHeaderComponent, ArtemisSharedCommonModule, NgxChartsModule, PieChartModule, TranslateDirective, RouterLink], }) export class CourseCardComponent implements OnChanges { protected readonly faArrowRight = faArrowRight; @@ -80,8 +87,6 @@ export class CourseCardComponent implements OnChanges { this.ngxDoughnutData[1].value = scoreNotReached; this.ngxDoughnutData = [...this.ngxDoughnutData]; } - - this.courseColor = this.course.color || this.ARTEMIS_DEFAULT_COLOR; } /** diff --git a/src/main/webapp/app/overview/course-card.scss b/src/main/webapp/app/overview/course-card.scss index 35d910731e5f..6525f3881181 100644 --- a/src/main/webapp/app/overview/course-card.scss +++ b/src/main/webapp/app/overview/course-card.scss @@ -10,49 +10,6 @@ background-color: var(--hover-slightly-darker-body-bg); } - .card-header { - // needed, otherwise hover effect won't work due to stretched-link class - z-index: 2; - position: relative; - height: 85px; - opacity: 1; - filter: alpha(opacity = 100); - transition: 0.15s; - background-color: var(--background-color-for-hover) !important; - // inner border radius : outer border radius - outer border thickness (8px - 1px) - border-top-left-radius: 7px; - border-top-right-radius: 7px; - - &:hover { - background-color: color-mix(in srgb, var(--background-color-for-hover), transparent 15%) !important; - } - - .container { - height: 80px; - - .row { - height: 80px; - } - } - - .card-title { - overflow: hidden; - padding-bottom: 1px; - // matches 4 lines - max-height: 76px; - } - - .course-circle { - // same size as the course icons - height: 65px; - min-width: 65px; - background-color: var(--course-image-bg); - border-radius: 50%; - display: inline-block; - color: var(--bs-body-color); - } - } - .card-body { .information-box-wrapper { height: 135px; diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index 86d8e31f26d3..f2ddf5708172 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -220,16 +220,20 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit this.course = this.courseStorageService.getCourse(this.courseId); this.isNotManagementView = !this.router.url.startsWith('/course-management'); // Notify the course access storage service that the course has been accessed - this.courseAccessStorageService.onCourseAccessed( - this.courseId, - CourseAccessStorageService.STORAGE_KEY, - CourseAccessStorageService.MAX_DISPLAYED_RECENTLY_ACCESSED_COURSES_OVERVIEW, - ); - this.courseAccessStorageService.onCourseAccessed( - this.courseId, - CourseAccessStorageService.STORAGE_KEY_DROPDOWN, - CourseAccessStorageService.MAX_DISPLAYED_RECENTLY_ACCESSED_COURSES_DROPDOWN, - ); + // If course is not active, it means that it is accessed from course archive, which should not + // be stored in local storage and therefore displayed in recently accessed + if (this.course && this.isCourseActive(this.course)) { + this.courseAccessStorageService.onCourseAccessed( + this.courseId, + CourseAccessStorageService.STORAGE_KEY, + CourseAccessStorageService.MAX_DISPLAYED_RECENTLY_ACCESSED_COURSES_OVERVIEW, + ); + this.courseAccessStorageService.onCourseAccessed( + this.courseId, + CourseAccessStorageService.STORAGE_KEY_DROPDOWN, + CourseAccessStorageService.MAX_DISPLAYED_RECENTLY_ACCESSED_COURSES_DROPDOWN, + ); + } await firstValueFrom(this.loadCourse()); await this.initAfterCourseLoad(); @@ -827,4 +831,15 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit this.isNavbarCollapsed = !this.isNavbarCollapsed; localStorage.setItem('navbar.collapseState', JSON.stringify(this.isNavbarCollapsed)); } + + /** + * A course is active if the end date is after the current date or + * end date is not set at all + * + * @param course The given course to be checked if it is active + * @returns true if the course is active, otherwise false + */ + isCourseActive(course: Course): boolean { + return course.endDate ? dayjs(course.endDate).isAfter(dayjs()) : true; + } } diff --git a/src/main/webapp/app/overview/courses-routing.module.ts b/src/main/webapp/app/overview/courses-routing.module.ts index 4cb31090febf..b40c8c952f85 100644 --- a/src/main/webapp/app/overview/courses-routing.module.ts +++ b/src/main/webapp/app/overview/courses-routing.module.ts @@ -12,6 +12,7 @@ import { CourseTutorialGroupDetailComponent } from './tutorial-group-details/cou import { ExamParticipationComponent } from 'app/exam/participate/exam-participation.component'; import { PendingChangesGuard } from 'app/shared/guard/pending-changes.guard'; import { CourseDashboardGuard } from 'app/overview/course-dashboard/course-dashboard-guard.service'; +import { CourseArchiveComponent } from './course-archive/course-archive.component'; const routes: Routes = [ { @@ -27,6 +28,15 @@ const routes: Routes = [ path: 'enroll', loadChildren: () => import('./course-registration/course-registration.module').then((m) => m.CourseRegistrationModule), }, + { + path: 'archive', + component: CourseArchiveComponent, + data: { + authorities: [Authority.USER], + pageTitle: 'overview.archive', + }, + canActivate: [UserRouteAccessService], + }, // /courses/:courseId/register is special, // because we won't have access to the course object before the user is registered, // so we need to load it outside the normal course routing diff --git a/src/main/webapp/app/overview/courses.component.html b/src/main/webapp/app/overview/courses.component.html index 5836c04911c3..b5dabf3a549c 100644 --- a/src/main/webapp/app/overview/courses.component.html +++ b/src/main/webapp/app/overview/courses.component.html @@ -24,7 +24,7 @@

    {{ nextRelevantExam.title }}

    {{ 'artemisApp.studentDashboard.title' | artemisTranslate }} ({{ regularCourses.length + recentlyAccessedCourses.length }})

    - + @@ -62,6 +62,13 @@

    }

    +@if (coursesLoaded) { +
    +
    +
     
    + +
    +} @if ((courses | searchFilter: ['title'] : searchCourseText).length > 0) { diff --git a/src/main/webapp/app/overview/courses.module.ts b/src/main/webapp/app/overview/courses.module.ts index 5dbae0d30fde..9d6693a17fdc 100644 --- a/src/main/webapp/app/overview/courses.module.ts +++ b/src/main/webapp/app/overview/courses.module.ts @@ -37,16 +37,9 @@ import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.co NgxChartsModule, PieChartModule, ArtemisSidebarModule, - SearchFilterComponent, - ], - declarations: [ - CoursesComponent, - CourseOverviewComponent, CourseCardComponent, - CourseExercisesComponent, - CourseLecturesComponent, - CourseLectureRowComponent, - CourseUnenrollmentModalComponent, + SearchFilterComponent, ], + declarations: [CoursesComponent, CourseOverviewComponent, CourseExercisesComponent, CourseLecturesComponent, CourseLectureRowComponent, CourseUnenrollmentModalComponent], }) export class ArtemisCoursesModule {} diff --git a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts index 14cc8732f244..af2c4cc17437 100644 --- a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts +++ b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts @@ -384,6 +384,7 @@ export class NavbarComponent implements OnInit, OnDestroy { live: 'artemisApp.submission.detail.title', courses: 'artemisApp.course.home.title', enroll: 'artemisApp.studentDashboard.enroll.title', + archive: 'artemisApp.course.archive.title', }; /** diff --git a/src/main/webapp/i18n/de/course.json b/src/main/webapp/i18n/de/course.json index 16c8ee6ae832..bde6bdceaa65 100644 --- a/src/main/webapp/i18n/de/course.json +++ b/src/main/webapp/i18n/de/course.json @@ -34,6 +34,14 @@ "isTestCourse": "Testkurs" } }, + "archive": { + "title": "Archiv", + "sort": "Sortieren", + "tip": "Das Archiv ermöglicht dir, all deine vergangenen Kurse, organisiert nach Semestern, anzusehen. Klicke auf ein Semester, um es zu erweitern und die Kurse zu sehen, in die du in diesem Zeitraum eingeschrieben warst.", + "noCoursesPreviousSemester": "Keine Kurse aus früheren Semestern gefunden", + "winterSemester": "Wintersemester 20{{ param }}", + "summerSemester": "Sommersemester 20{{ param }}" + }, "showActive": "Nur aktive Kurse anzeigen", "totalScore": "Gesamtergebnis:", "title": "Titel", diff --git a/src/main/webapp/i18n/de/global.json b/src/main/webapp/i18n/de/global.json index 3b8195fea41d..7d18d0c51e03 100644 --- a/src/main/webapp/i18n/de/global.json +++ b/src/main/webapp/i18n/de/global.json @@ -352,6 +352,7 @@ "statistics": "Kursstatistiken", "exams": "Klausuren", "communication": "Kommunikation", + "archive": "Kursarchiv", "faq": "FAQ" }, "connectionStatus": { diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index a14592b067dc..3c15b1c3987c 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -12,10 +12,14 @@ "cardTitle": "Deine insgesamte Punktzahl:", "noStatistics": "Keine Statistik verfügbar", "cardNoExerciseLabel": "Keine Übung geplant", - "cardExerciseLabel": "Nächste Übung", + "cardExerciseLabel": "Nächste Übung:", "points": "{{ totalAbsoluteScore }} / {{ totalReachableScore }} Punkte", "cardScore": "Punktzahl", "cardManageCourse": "Kurs Verwalten", + "archive": { + "oldCourses": "Suchst du nach alten Kursen? Klicke", + "here": "hier" + }, "noCoursesFound": "Keine Kurse gefunden", "enroll": { "title": "Kurseinschreibung", diff --git a/src/main/webapp/i18n/en/course.json b/src/main/webapp/i18n/en/course.json index d4246b664a54..922436f0889d 100644 --- a/src/main/webapp/i18n/en/course.json +++ b/src/main/webapp/i18n/en/course.json @@ -34,6 +34,14 @@ "isTestCourse": "Test Course" } }, + "archive": { + "title": "Archive", + "sort": "Sort", + "tip": "The archive enables you to view all your past courses, organized by semester. Click on a semester to expand and see the courses you were enrolled in during that period.", + "noCoursesPreviousSemester": "No courses found from previous semesters", + "winterSemester": "Winter semester 20{{ param }}", + "summerSemester": "Summer semester 20{{ param }}" + }, "showActive": "Show only active courses", "totalScore": "Total Score:", "title": "Title", diff --git a/src/main/webapp/i18n/en/global.json b/src/main/webapp/i18n/en/global.json index 623c55bb042b..fa014d6128ef 100644 --- a/src/main/webapp/i18n/en/global.json +++ b/src/main/webapp/i18n/en/global.json @@ -352,6 +352,7 @@ "statistics": "Course statistics", "exams": "Exams", "communication": "Communication", + "archive": "Course archive", "faq": "FAQ" }, "connectionStatus": { diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 58b2effd112c..1e51fba7c618 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -12,10 +12,14 @@ "cardTitle": "Your overall points:", "noStatistics": "No statistics available", "cardNoExerciseLabel": "No exercise planned", - "cardExerciseLabel": "Next Exercise", + "cardExerciseLabel": "Next Exercise:", "points": "{{ totalAbsoluteScore }} / {{ totalReachableScore }} Points", "cardScore": "Score", "cardManageCourse": "Manage Course", + "archive": { + "oldCourses": "Looking for old courses? Click", + "here": "here" + }, "noCoursesFound": "No Courses Found", "enroll": { "title": "Course Enrollment", diff --git a/src/test/java/de/tum/cit/aet/artemis/core/util/CourseTestService.java b/src/test/java/de/tum/cit/aet/artemis/core/util/CourseTestService.java index c80195c2b8aa..e5634a81d5e7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/util/CourseTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/util/CourseTestService.java @@ -90,6 +90,7 @@ import de.tum.cit.aet.artemis.core.domain.CourseInformationSharingConfiguration; import de.tum.cit.aet.artemis.core.domain.Organization; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.dto.CourseForArchiveDTO; import de.tum.cit.aet.artemis.core.dto.CourseForDashboardDTO; import de.tum.cit.aet.artemis.core.dto.CourseForImportDTO; import de.tum.cit.aet.artemis.core.dto.CourseManagementDetailViewDTO; @@ -3383,4 +3384,58 @@ public void testGetCoursesForImport() throws Exception { assertThat(found).as("Course is available").isPresent(); } } + + // Test + public void testGetAllCoursesForCourseArchiveWithNonNullSemestersAndEndDate() throws Exception { + List expectedOldCourses = new ArrayList<>(); + for (int i = 1; i <= 4; i++) { + expectedOldCourses.add(courseUtilService.createCourse((long) i)); + } + + expectedOldCourses.get(0).setSemester("SS20"); + expectedOldCourses.get(0).setEndDate(ZonedDateTime.now().minusDays(10)); + expectedOldCourses.get(1).setSemester("SS21"); + expectedOldCourses.get(1).setEndDate(ZonedDateTime.now().minusDays(10)); + expectedOldCourses.get(2).setSemester("WS21/22"); + expectedOldCourses.get(2).setEndDate(ZonedDateTime.now().minusDays(10)); + expectedOldCourses.get(3).setSemester(null); // will be filtered out + + for (Course oldCourse : expectedOldCourses) { + courseRepo.save(oldCourse); + } + + final Set actualOldCourses = request.getSet("/api/courses/for-archive", HttpStatus.OK, CourseForArchiveDTO.class); + assertThat(actualOldCourses).as("Course archive has 3 courses").hasSize(3); + assertThat(actualOldCourses).as("Course archive has the correct semesters").extracting("semester").containsExactlyInAnyOrder(expectedOldCourses.get(0).getSemester(), + expectedOldCourses.get(1).getSemester(), expectedOldCourses.get(2).getSemester()); + assertThat(actualOldCourses).as("Course archive got the correct courses").extracting("id").containsExactlyInAnyOrder(expectedOldCourses.get(0).getId(), + expectedOldCourses.get(1).getId(), expectedOldCourses.get(2).getId()); + Optional notFound = actualOldCourses.stream().filter(c -> Objects.equals(c.id(), expectedOldCourses.get(3).getId())).findFirst(); + assertThat(notFound).as("Course archive did not fetch the last course").isNotPresent(); + } + + // Test + public void testGetAllCoursesForCourseArchiveForUnenrolledStudent() throws Exception { + Course course1 = courseUtilService.createCourse((long) 1); + course1.setSemester("SS20"); + course1.setEndDate(ZonedDateTime.now().minusDays(10)); + courseRepo.save(course1); + + Course course2 = courseUtilService.createCourse((long) 2); + course2.setSemester("SS21"); + course2.setEndDate(ZonedDateTime.now().minusDays(10)); + courseRepo.save(course2); + + Course course3 = courseUtilService.createCourse((long) 3); + course3.setSemester("WS21/22"); + course3.setEndDate(ZonedDateTime.now().minusDays(10)); + courseRepo.save(course3); + + // remove student from all courses + removeAllGroupsFromStudent1(); + + final Set actualCoursesForStudent = request.getSet("/api/courses/for-archive", HttpStatus.OK, CourseForArchiveDTO.class); + assertThat(actualCoursesForStudent).as("Course archive does not show any courses to the user removed from these courses").hasSize(0); + } + } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java index ffe1a7bf34a6..f804a66196e8 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java @@ -1041,4 +1041,16 @@ void testGetCoursesForImport_asAdmin() throws Exception { void testFindAllOnlineCoursesForLtiDashboard() throws Exception { courseTestService.testFindAllOnlineCoursesForLtiDashboard(); } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testGetAllCoursesForCourseArchiveWithNonNullSemesters() throws Exception { + courseTestService.testGetAllCoursesForCourseArchiveWithNonNullSemestersAndEndDate(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testGetAllCoursesForCourseArchiveForUnenrolledStudent() throws Exception { + courseTestService.testGetAllCoursesForCourseArchiveForUnenrolledStudent(); + } } diff --git a/src/test/javascript/spec/component/course/course-archive.component.spec.ts b/src/test/javascript/spec/component/course/course-archive.component.spec.ts new file mode 100644 index 000000000000..8bc41d3e81b6 --- /dev/null +++ b/src/test/javascript/spec/component/course/course-archive.component.spec.ts @@ -0,0 +1,277 @@ +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpTestingController } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { TranslateService } from '@ngx-translate/core'; +import { CourseManagementService } from 'app/course/manage/course-management.service'; +import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; +import { MockComponent, MockDirective, MockPipe } from 'ng-mocks'; +import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; +import { MockHasAnyAuthorityDirective } from '../../helpers/mocks/directive/mock-has-any-authority.directive'; +import { MockSyncStorage } from '../../helpers/mocks/service/mock-sync-storage.service'; +import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; +import { ArtemisTestModule } from '../../test.module'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { SortByDirective } from 'app/shared/sort/sort-by.directive'; +import { SortDirective } from 'app/shared/sort/sort.directive'; +import { of } from 'rxjs'; +import { By } from '@angular/platform-browser'; +import { CourseArchiveComponent } from 'app/overview/course-archive/course-archive.component'; +import { CourseCardHeaderComponent } from 'app/overview/course-card-header/course-card-header.component'; +import { SearchFilterPipe } from 'app/shared/pipes/search-filter.pipe'; +import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; +import { CourseForArchiveDTO } from 'app/course/manage/course-for-archive-dto'; + +const course1 = { id: 1, semester: 'WS21/22', title: 'iPraktikum' } as CourseForArchiveDTO; +const course2 = { id: 2, semester: 'WS21/22' } as CourseForArchiveDTO; +const course3 = { id: 3, semester: 'SS22' } as CourseForArchiveDTO; +const course4 = { id: 4, semester: 'SS22' } as CourseForArchiveDTO; +const course5 = { id: 5, semester: 'WS23/24' } as CourseForArchiveDTO; +const course6 = { id: 6, semester: 'SS19' } as CourseForArchiveDTO; +const course7 = { id: 7, semester: 'WS22/23' } as CourseForArchiveDTO; +const courses: CourseForArchiveDTO[] = [course1, course2, course3, course4, course5, course6, course7]; + +describe('CourseArchiveComponent', () => { + let component: CourseArchiveComponent; + let fixture: ComponentFixture; + let courseService: CourseManagementService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [ + CourseArchiveComponent, + SearchFilterPipe, + SearchFilterComponent, + MockDirective(MockHasAnyAuthorityDirective), + MockPipe(ArtemisTranslatePipe), + MockDirective(SortDirective), + MockDirective(SortByDirective), + MockPipe(ArtemisDatePipe), + MockComponent(CourseCardHeaderComponent), + ], + providers: [ + { provide: LocalStorageService, useClass: MockSyncStorage }, + { provide: SessionStorageService, useClass: MockSyncStorage }, + { provide: TranslateService, useClass: MockTranslateService }, + ], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(CourseArchiveComponent); + component = fixture.componentInstance; + courseService = TestBed.inject(CourseManagementService); + httpMock = TestBed.inject(HttpTestingController); + fixture.detectChanges(); + }); + }); + + afterEach(() => { + component.ngOnDestroy(); + jest.restoreAllMocks(); + }); + + describe('onInit', () => { + it('should call loadArchivedCourses on init', () => { + const loadArchivedCoursesSpy = jest.spyOn(component, 'loadArchivedCourses'); + + component.ngOnInit(); + + expect(loadArchivedCoursesSpy).toHaveBeenCalledOnce(); + }); + + it('should load archived courses on init', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + + component.ngOnInit(); + + expect(component.courses).toEqual(courses); + expect(component.courses).toHaveLength(7); + }); + + it('should handle an empty response body correctly when fetching all courses for archive', () => { + const emptyCourses: CourseForArchiveDTO[] = []; + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + + const req = httpMock.expectOne({ method: 'GET', url: `api/courses/for-archive` }); + component.ngOnInit(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + req.flush(null); + expect(component.courses).toStrictEqual(emptyCourses); + }); + + it('should sort the name of the semesters uniquely', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + component.ngOnInit(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + + expect(component.semesters).toHaveLength(5); + expect(component.semesters[0]).toBe('WS23/24'); + expect(component.semesters[1]).toBe('WS22/23'); + expect(component.semesters[2]).toBe('SS22'); + expect(component.semesters[3]).toBe('WS21/22'); + expect(component.semesters[4]).toBe('SS19'); + }); + + it('should map courses into semesters', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + component.ngOnInit(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + + expect(component.coursesBySemester).toStrictEqual({ + 'WS23/24': [course5], + 'WS22/23': [course7], + SS22: [course3, course4], + 'WS21/22': [course2, course1], + SS19: [course6], + }); + }); + + it('should initialize collapse state of semesters correctly', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + component.ngOnInit(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + + // we expand all semesters at first + expect(component.semesterCollapsed).toStrictEqual({ + 'WS23/24': false, + 'WS22/23': false, + SS22: false, + 'WS21/22': false, + SS19: false, + }); + }); + + it('should initialize translate of semesters correctly', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + component.ngOnInit(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + + expect(component.fullFormOfSemesterStrings).toStrictEqual({ + 'WS23/24': 'artemisApp.course.archive.winterSemester', + 'WS22/23': 'artemisApp.course.archive.winterSemester', + SS22: 'artemisApp.course.archive.summerSemester', + 'WS21/22': 'artemisApp.course.archive.winterSemester', + SS19: 'artemisApp.course.archive.summerSemester', + }); + }); + + it('should collapse semester groups based on the search value correctly', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + component.ngOnInit(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + + const expandOrCollapseBasedOnSearchValueSpy = jest.spyOn(component, 'expandOrCollapseBasedOnSearchValue'); + component.setSearchValue('iPraktikum'); + + expect(expandOrCollapseBasedOnSearchValueSpy).toHaveBeenCalledOnce(); + // Every semester accordion should be collapsed except WS21/22, because iPraktikum is in semester WS21/22 + expect(component.semesterCollapsed).toStrictEqual({ + 'WS23/24': true, + 'WS22/23': true, + SS22: true, + 'WS21/22': false, + SS19: true, + }); + }); + + it('should toggle sort order and update the icon accordingly', fakeAsync(() => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + component.ngOnInit(); + fixture.detectChanges(); + tick(); + + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + expect(component.courses).toBeDefined(); + expect(component.courses).toHaveLength(7); + + const onSortSpy = jest.spyOn(component, 'onSort'); + const button = fixture.debugElement.nativeElement.querySelector('#sort-test'); + + expect(button).not.toBeNull(); + button.click(); + fixture.detectChanges(); + + expect(onSortSpy).toHaveBeenCalled(); + expect(component.isSortAscending).toBeFalse(); + expect(component.semesters[4]).toBe('WS23/24'); + expect(component.semesters[3]).toBe('WS22/23'); + expect(component.semesters[2]).toBe('SS22'); + expect(component.semesters[1]).toBe('WS21/22'); + expect(component.semesters[0]).toBe('SS19'); + + const iconComponent = fixture.debugElement.query(By.css('#icon-test-down')).componentInstance; + + expect(iconComponent).not.toBeNull(); + expect(iconComponent.icon).toBe(component.faArrowUp19); + })); + + it('should find the correct course and call toggle', fakeAsync(() => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + expect(component.courses).toHaveLength(7); + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + + // iPraktikum is in semester-group-3 : WS21/22 + const button = fixture.debugElement.nativeElement.querySelector('#semester-group-3'); + const toggleCollapseStateSpy = jest.spyOn(component, 'toggleCollapseState'); + component.setSearchValue('iPraktikum'); + const courseFound = component.isCourseFoundInSemester('WS21/22'); + expect(courseFound).toBeTrue(); + expect(button).not.toBeNull(); + button.click(); + expect(toggleCollapseStateSpy).toHaveBeenCalledOnce(); + })); + + it('should initialize collapse state correctly', () => { + const getCoursesForArchiveSpy = jest.spyOn(courseService, 'getCoursesForArchive'); + getCoursesForArchiveSpy.mockReturnValue(of(new HttpResponse({ body: courses, headers: new HttpHeaders() }))); + const mapCoursesIntoSemestersSpy = jest.spyOn(component, 'mapCoursesIntoSemesters'); + + component.ngOnInit(); + expect(component.courses).toHaveLength(7); + expect(getCoursesForArchiveSpy).toHaveBeenCalledOnce(); + expect(mapCoursesIntoSemestersSpy).toHaveBeenCalledOnce(); + const getCollapseStateForSemestersSpy = jest.spyOn(component, 'getCollapseStateForSemesters'); + component.setSearchValue(''); + expect(getCollapseStateForSemestersSpy).toHaveBeenCalledOnce(); + + expect(component.semesterCollapsed).toStrictEqual({ + 'WS23/24': false, + 'WS22/23': false, + SS22: false, + 'WS21/22': false, + SS19: false, + }); + }); + }); +}); diff --git a/src/test/javascript/spec/component/course/course-overview.component.spec.ts b/src/test/javascript/spec/component/course/course-overview.component.spec.ts index 8a92520473bf..ab342d14828c 100644 --- a/src/test/javascript/spec/component/course/course-overview.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-overview.component.spec.ts @@ -13,7 +13,6 @@ import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { CourseExerciseRowComponent } from 'app/overview/course-exercises/course-exercise-row.component'; import { CourseExercisesComponent } from 'app/overview/course-exercises/course-exercises.component'; import { CourseRegistrationComponent } from 'app/overview/course-registration/course-registration.component'; -import { CourseCardComponent } from 'app/overview/course-card.component'; import dayjs from 'dayjs/esm'; import { Exercise } from 'app/entities/exercise.model'; import { DueDateStat } from 'app/course/dashboards/due-date-stat.model'; @@ -184,7 +183,6 @@ describe('CourseOverviewComponent', () => { MockComponent(CourseExerciseRowComponent), MockComponent(CourseExercisesComponent), MockComponent(CourseRegistrationComponent), - MockComponent(CourseCardComponent), MockComponent(SecuredImageComponent), ], providers: [ diff --git a/src/test/javascript/spec/component/course/course.component.spec.ts b/src/test/javascript/spec/component/course/course.component.spec.ts index 925a54864297..2c01e4fc0273 100644 --- a/src/test/javascript/spec/component/course/course.component.spec.ts +++ b/src/test/javascript/spec/component/course/course.component.spec.ts @@ -274,4 +274,26 @@ describe('CoursesComponent', () => { expect(component.courses).toEqual([course1, course2, course6]); expect(component.nextRelevantExams).toEqual([]); })); + + it('should initialize search course text correctly', () => { + const searchedCourse = 'Test Course'; + component.setSearchValue('Test Course'); + expect(searchedCourse).toBe(component.searchCourseText); + }); + + it('should adjust sort direction by clicking on sort icon', () => { + const findAllForDashboardSpy = jest.spyOn(courseService, 'findAllForDashboard'); + findAllForDashboardSpy.mockReturnValue(of(new HttpResponse({ body: coursesDashboard, headers: new HttpHeaders() }))); + component.ngOnInit(); + + expect(findAllForDashboardSpy).toHaveBeenCalledOnce(); + expect(component.courses).toEqual(courses); + expect(component.isSortAscending).toBeTrue(); + + const onSortSpy = jest.spyOn(component, 'onSort'); + const button = fixture.debugElement.nativeElement.querySelector('#test-sort'); + button.click(); + expect(onSortSpy).toHaveBeenCalledOnce(); + expect(component.isSortAscending).toBeFalse(); + }); }); diff --git a/src/test/javascript/spec/component/overview/course-card.component.spec.ts b/src/test/javascript/spec/component/overview/course-card.component.spec.ts index fbe16f160c51..bf2403aecf23 100644 --- a/src/test/javascript/spec/component/overview/course-card.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-card.component.spec.ts @@ -15,6 +15,7 @@ import { PieChartModule } from '@swimlane/ngx-charts'; import { TranslateDirective } from 'app/shared/language/translate.directive'; import { ScoresStorageService } from 'app/course/course-scores/scores-storage.service'; import { CourseScores } from 'app/course/course-scores/course-scores'; +import { CourseCardHeaderComponent } from 'app/overview/course-card-header/course-card-header.component'; describe('CourseCardComponent', () => { let fixture: ComponentFixture; @@ -40,6 +41,7 @@ describe('CourseCardComponent', () => { MockPipe(ArtemisTimeAgoPipe), MockRouterLinkDirective, MockComponent(SecuredImageComponent), + MockComponent(CourseCardHeaderComponent), MockDirective(TranslateDirective), ], }) diff --git a/src/test/javascript/spec/integration/guided-tour/guided-tour.integration.spec.ts b/src/test/javascript/spec/integration/guided-tour/guided-tour.integration.spec.ts index 3ebe67f4a26e..d439a5cb2347 100644 --- a/src/test/javascript/spec/integration/guided-tour/guided-tour.integration.spec.ts +++ b/src/test/javascript/spec/integration/guided-tour/guided-tour.integration.spec.ts @@ -20,6 +20,7 @@ import { ThemeSwitchComponent } from 'app/core/theme/theme-switch.component'; import { User } from 'app/core/user/user.model'; import { MockHasAnyAuthorityDirective } from '../../helpers/mocks/directive/mock-has-any-authority.directive'; import { CourseCardComponent } from 'app/overview/course-card.component'; +import { CourseCardHeaderComponent } from 'app/overview/course-card-header/course-card-header.component'; import { Course } from 'app/entities/course.model'; import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; @@ -79,6 +80,7 @@ describe('Guided tour integration', () => { FooterComponent, NotificationSidebarComponent, MockHasAnyAuthorityDirective, + MockComponent(CourseCardHeaderComponent), MockComponent(CourseRegistrationComponent), MockComponent(CourseExerciseRowComponent), MockComponent(LoadingNotificationComponent), From 982060fe43798d4b06ffcc10a5217cf387522727 Mon Sep 17 00:00:00 2001 From: Tim Cremer <65229601+cremertim@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:08:48 +0200 Subject: [PATCH 07/18] Communication: Remove announcements from unresolved filter (#9561) --- .../course-wide-search.component.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/overview/course-conversations/course-wide-search/course-wide-search.component.ts b/src/main/webapp/app/overview/course-conversations/course-wide-search/course-wide-search.component.ts index 9e07517c3f8a..7c4ce294e0ac 100644 --- a/src/main/webapp/app/overview/course-conversations/course-wide-search/course-wide-search.component.ts +++ b/src/main/webapp/app/overview/course-conversations/course-wide-search/course-wide-search.component.ts @@ -17,7 +17,7 @@ import { faCircleNotch, faEnvelope, faFilter, faLongArrowAltDown, faLongArrowAlt import { FormBuilder, FormGroup } from '@angular/forms'; import { Subject, takeUntil } from 'rxjs'; import { Course } from 'app/entities/course.model'; -import { getAsChannelDTO } from 'app/entities/metis/conversation/channel.model'; +import { ChannelDTO, getAsChannelDTO } from 'app/entities/metis/conversation/channel.model'; import { Post } from 'app/entities/metis/post.model'; import { MetisService } from 'app/shared/metis/metis.service'; import { MetisConversationService } from 'app/shared/metis/metis-conversation.service'; @@ -150,10 +150,20 @@ export class CourseWideSearchComponent implements OnInit, AfterViewInit, OnDestr pageSize: 50, }; this.metisConversationService.conversationsOfUser$.pipe(takeUntil(this.ngUnsubscribe)).subscribe((conversations: ConversationDTO[]) => { - this.currentPostContextFilter!.courseWideChannelIds = conversations.map((conversation) => conversation!.id!); + this.currentPostContextFilter!.courseWideChannelIds = conversations + .filter((conversation) => !(this.currentPostContextFilter?.filterToUnresolved && this.conversationIsAnnouncement(conversation))) + .map((conversation) => conversation.id!); }); } + conversationIsAnnouncement(conversation: ConversationDTO) { + if (conversation.type === 'channel') { + const channel = conversation as ChannelDTO; + return channel.isAnnouncementChannel; + } + return false; + } + postsTrackByFn = (index: number, post: Post): number => post.id!; setPostForThread(post: Post) { From aa6be64757ea9eff45375e1bd95cc783d8443077 Mon Sep 17 00:00:00 2001 From: Timor Morrien Date: Thu, 24 Oct 2024 08:20:33 +0200 Subject: [PATCH 08/18] Quiz exercises: Fix an error after using the practice mode (#9571) --- .../quiz/repository/QuizSubmissionRepository.java | 12 +++++++++++- .../artemis/quiz/service/QuizSubmissionService.java | 2 +- .../artemis/quiz/web/QuizParticipationResource.java | 11 ++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/repository/QuizSubmissionRepository.java b/src/main/java/de/tum/cit/aet/artemis/quiz/repository/QuizSubmissionRepository.java index 4202d525190b..39cd106f67b2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/repository/QuizSubmissionRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/repository/QuizSubmissionRepository.java @@ -3,6 +3,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; +import java.util.List; import java.util.Optional; import java.util.Set; @@ -37,7 +38,16 @@ public interface QuizSubmissionRepository extends ArtemisJpaRepository findWithEagerSubmittedAnswersByParticipationId(long participationId); + List findWithEagerSubmittedAnswersByParticipationId(long participationId); + + @Query(""" + SELECT submission + FROM QuizSubmission submission + LEFT JOIN FETCH submission.submittedAnswers + JOIN submission.results r + WHERE r.id = :resultId + """) + Optional findWithEagerSubmittedAnswersByResultId(@Param("resultId") long resultId); /** * Retrieve QuizSubmission for given quiz batch and studentLogin diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizSubmissionService.java b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizSubmissionService.java index 3892a0e8e44e..481648fb9636 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizSubmissionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/service/QuizSubmissionService.java @@ -147,7 +147,7 @@ public void calculateAllResults(long quizExerciseId) { log.info("Calculating results for quiz {}", quizExercise.getId()); studentParticipationRepository.findByExerciseId(quizExercise.getId()).forEach(participation -> { participation.setExercise(quizExercise); - Optional quizSubmissionOptional = quizSubmissionRepository.findWithEagerSubmittedAnswersByParticipationId(participation.getId()); + Optional quizSubmissionOptional = quizSubmissionRepository.findWithEagerSubmittedAnswersByParticipationId(participation.getId()).stream().findFirst(); if (quizSubmissionOptional.isEmpty()) { return; diff --git a/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizParticipationResource.java index 41c6fc8173c9..fc2b4d3b3c94 100644 --- a/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/quiz/web/QuizParticipationResource.java @@ -69,6 +69,8 @@ public QuizParticipationResource(QuizExerciseRepository quizExerciseRepository, /** * POST /quiz-exercises/{exerciseId}/start-participation : start the quiz exercise participation + * TODO: This endpoint is also called when viewing the result of a quiz exercise. + * TODO: This does not make any sense, as the participation is already started. * * @param exerciseId the id of the quiz exercise * @return The created participation @@ -92,7 +94,14 @@ public ResponseEntity startParticipation(@PathVariable Long // NOTE: starting exercise prevents that two participation will exist, but ensures that a submission is created var result = resultRepository.findFirstByParticipationIdAndRatedOrderByCompletionDateDesc(participation.getId(), true).orElse(new Result()); - result.setSubmission(quizSubmissionRepository.findWithEagerSubmittedAnswersByParticipationId(participation.getId()).orElseThrow()); + if (result.getId() == null) { + // Load the live submission of the participation + result.setSubmission(quizSubmissionRepository.findWithEagerSubmittedAnswersByParticipationId(participation.getId()).stream().findFirst().orElseThrow()); + } + else { + // Load the actual submission of the result + result.setSubmission(quizSubmissionRepository.findWithEagerSubmittedAnswersByResultId(result.getId()).orElseThrow()); + } participation.setResults(Set.of(result)); participation.setExercise(exercise); From e6aaae35ee0affe2d6df887cdba21fb930b869c3 Mon Sep 17 00:00:00 2001 From: Aniruddh Zaveri <92953467+az108@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:21:28 +0200 Subject: [PATCH 09/18] Programming exercises: Enhance filtering and sorting for error analysis (#9315) --- .../dto/FeedbackAnalysisResponseDTO.java | 11 + .../assessment/dto/FeedbackDetailDTO.java | 2 +- .../assessment/dto/FeedbackPageableDTO.java | 48 +++++ .../assessment/service/ResultService.java | 104 ++++++++-- .../assessment/web/ResultResource.java | 61 +++++- .../cit/aet/artemis/core/util/PageUtil.java | 28 ++- .../StudentParticipationRepository.java | 118 ++++++++--- .../ProgrammingExerciseRepository.java | 11 + .../ProgrammingExerciseTaskRepository.java | 5 +- .../service/ProgrammingExerciseService.java | 2 +- .../ProgrammingExerciseTaskService.java | 4 +- .../ProgrammingExerciseTaskResource.java | 5 +- .../feedback-filter-modal.component.html | 70 +++++++ .../Modal/feedback-filter-modal.component.ts | 90 ++++++++ .../Modal/feedback-modal.component.html | 44 ++++ .../Modal/feedback-modal.component.ts | 15 ++ .../feedback-analysis.component.html | 76 ++++++- .../feedback-analysis.component.scss | 9 + .../feedback-analysis.component.ts | 170 +++++++++++++-- .../feedback-analysis.service.ts | 31 ++- .../webapp/i18n/de/programmingExercise.json | 18 +- .../webapp/i18n/en/programmingExercise.json | 18 +- .../ResultServiceIntegrationTest.java | 88 ++++++-- ...rogrammingExerciseTaskIntegrationTest.java | 3 +- .../feedback-analysis.component.spec.ts | 193 ++++++++++++++++-- .../feedback-analysis.service.spec.ts | 46 ++++- .../feedback-filter-modal.component.spec.ts | 97 +++++++++ .../modals/feedback-modal.component.spec.ts | 44 ++++ 28 files changed, 1264 insertions(+), 147 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackPageableDTO.java create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.html create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.ts create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.ts create mode 100644 src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.scss create mode 100644 src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-filter-modal.component.spec.ts create mode 100644 src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-modal.component.spec.ts diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java new file mode 100644 index 000000000000..d913f0c96e3f --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackAnalysisResponseDTO.java @@ -0,0 +1,11 @@ +package de.tum.cit.aet.artemis.assessment.dto; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record FeedbackAnalysisResponseDTO(SearchResultPageDTO feedbackDetails, long totalItems, int totalAmountOfTasks, List testCaseNames) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackDetailDTO.java b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackDetailDTO.java index 23ea64b409b4..7b3fd09ad57d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackDetailDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackDetailDTO.java @@ -3,5 +3,5 @@ import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record FeedbackDetailDTO(long count, double relativeCount, String detailText, String testCaseName, int taskNumber) { +public record FeedbackDetailDTO(long count, double relativeCount, String detailText, String testCaseName, String taskNumber, String errorCategory) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackPageableDTO.java b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackPageableDTO.java new file mode 100644 index 000000000000..c63f9b5540f7 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/dto/FeedbackPageableDTO.java @@ -0,0 +1,48 @@ +package de.tum.cit.aet.artemis.assessment.dto; + +import java.util.List; + +import de.tum.cit.aet.artemis.core.dto.pageablesearch.PageableSearchDTO; + +public class FeedbackPageableDTO extends PageableSearchDTO { + + private List filterTasks; + + private List filterTestCases; + + private String[] filterOccurrence; + + private String searchTerm; + + public List getFilterTasks() { + return filterTasks; + } + + public void setFilterTasks(List filterTasks) { + this.filterTasks = filterTasks; + } + + public List getFilterTestCases() { + return filterTestCases; + } + + public void setFilterTestCases(List filterTestCases) { + this.filterTestCases = filterTestCases; + } + + public String[] getFilterOccurrence() { + return filterOccurrence; + } + + public void setFilterOccurrence(String[] filterOccurrence) { + this.filterOccurrence = filterOccurrence; + } + + public String getSearchTerm() { + return searchTerm != null ? searchTerm : ""; + } + + public void setSearchTerm(String searchTerm) { + this.searchTerm = searchTerm; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java index 07b038b9cab2..a9e9050d5e2c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/service/ResultService.java @@ -17,10 +17,12 @@ import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; +import org.apache.commons.lang3.StringUtils; import org.hibernate.Hibernate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import de.tum.cit.aet.artemis.assessment.domain.AssessmentType; @@ -28,7 +30,9 @@ import de.tum.cit.aet.artemis.assessment.domain.FeedbackType; import de.tum.cit.aet.artemis.assessment.domain.LongFeedbackText; import de.tum.cit.aet.artemis.assessment.domain.Result; +import de.tum.cit.aet.artemis.assessment.dto.FeedbackAnalysisResponseDTO; import de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO; +import de.tum.cit.aet.artemis.assessment.dto.FeedbackPageableDTO; import de.tum.cit.aet.artemis.assessment.repository.ComplaintRepository; import de.tum.cit.aet.artemis.assessment.repository.ComplaintResponseRepository; import de.tum.cit.aet.artemis.assessment.repository.FeedbackRepository; @@ -40,10 +44,12 @@ import de.tum.cit.aet.artemis.buildagent.dto.ResultBuildJob; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; +import de.tum.cit.aet.artemis.core.util.PageUtil; import de.tum.cit.aet.artemis.exam.domain.Exam; import de.tum.cit.aet.artemis.exam.repository.StudentExamRepository; import de.tum.cit.aet.artemis.exercise.domain.Exercise; @@ -53,11 +59,14 @@ import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository; import de.tum.cit.aet.artemis.exercise.service.ExerciseDateService; import de.tum.cit.aet.artemis.lti.service.LtiNewResultService; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseTestCase; import de.tum.cit.aet.artemis.programming.domain.build.BuildPlanType; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTask; import de.tum.cit.aet.artemis.programming.repository.BuildJobRepository; +import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseStudentParticipationRepository; import de.tum.cit.aet.artemis.programming.repository.SolutionProgrammingExerciseParticipationRepository; import de.tum.cit.aet.artemis.programming.repository.TemplateProgrammingExerciseParticipationRepository; @@ -110,6 +119,8 @@ public class ResultService { private final ProgrammingExerciseTaskService programmingExerciseTaskService; + private final ProgrammingExerciseRepository programmingExerciseRepository; + public ResultService(UserRepository userRepository, ResultRepository resultRepository, Optional ltiNewResultService, ResultWebsocketService resultWebsocketService, ComplaintResponseRepository complaintResponseRepository, RatingRepository ratingRepository, FeedbackRepository feedbackRepository, LongFeedbackTextRepository longFeedbackTextRepository, ComplaintRepository complaintRepository, @@ -118,7 +129,7 @@ public ResultService(UserRepository userRepository, ResultRepository resultRepos SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, StudentExamRepository studentExamRepository, BuildJobRepository buildJobRepository, BuildLogEntryService buildLogEntryService, StudentParticipationRepository studentParticipationRepository, - ProgrammingExerciseTaskService programmingExerciseTaskService) { + ProgrammingExerciseTaskService programmingExerciseTaskService, ProgrammingExerciseRepository programmingExerciseRepository) { this.userRepository = userRepository; this.resultRepository = resultRepository; this.ltiNewResultService = ltiNewResultService; @@ -139,6 +150,7 @@ public ResultService(UserRepository userRepository, ResultRepository resultRepos this.buildLogEntryService = buildLogEntryService; this.studentParticipationRepository = studentParticipationRepository; this.programmingExerciseTaskService = programmingExerciseTaskService; + this.programmingExerciseRepository = programmingExerciseRepository; } /** @@ -530,31 +542,85 @@ private Result shouldSaveResult(@NotNull Result result, boolean shouldSave) { } /** - * Retrieves aggregated feedback details for a given exercise, calculating relative counts based on the total number of distinct results. - * The task numbers are assigned based on the associated test case names, using the set of tasks fetched from the database. + * Retrieves paginated and filtered aggregated feedback details for a given exercise. *
    * For each feedback detail: * 1. The relative count is calculated as a percentage of the total number of distinct results for the exercise. - * 2. The task number is determined by matching the test case name with the tasks. + * 2. The task numbers are assigned based on the associated test case names. A mapping between test cases and tasks is created using the set of tasks retrieved from the + * database. + *
    + * Filtering: + * - **Search term**: Filters feedback details by the search term (case-insensitive). + * - **Test case names**: Filters feedback based on specific test case names (if provided). + * - **Task names**: Maps provided task numbers to task names and filters feedback based on the test cases associated with those tasks. + * - **Occurrences**: Filters feedback where the number of occurrences (COUNT) is between the provided minimum and maximum values (inclusive). + *
    + * Pagination and sorting: + * - Sorting is applied based on the specified column and order (ascending or descending). + * - The result is paginated based on the provided page number and page size. * * @param exerciseId The ID of the exercise for which feedback details should be retrieved. - * @return A list of FeedbackDetailDTO objects, each containing: - * - feedback count, - * - relative count (as a percentage of distinct results), - * - detail text, - * - test case name, - * - determined task number (based on the test case name). + * @param data The {@link FeedbackPageableDTO} containing page number, page size, search term, sorting options, and filtering parameters (task names, test cases, + * occurrence range). + * @return A {@link FeedbackAnalysisResponseDTO} object containing: + * - A {@link SearchResultPageDTO} of paginated feedback details. + * - The total number of distinct results for the exercise. + * - The total number of tasks associated with the feedback. + * - A list of test case names included in the feedback. */ - public List findAggregatedFeedbackByExerciseId(long exerciseId) { + public FeedbackAnalysisResponseDTO getFeedbackDetailsOnPage(long exerciseId, FeedbackPageableDTO data) { + + // 1. Fetch programming exercise with associated test cases + ProgrammingExercise programmingExercise = programmingExerciseRepository.findWithTestCasesByIdElseThrow(exerciseId); + long distinctResultCount = studentParticipationRepository.countDistinctResultsByExerciseId(exerciseId); - Set tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId); - List feedbackDetails = studentParticipationRepository.findAggregatedFeedbackByExerciseId(exerciseId); - - return feedbackDetails.stream().map(detail -> { - double relativeCount = (detail.count() * 100.0) / distinctResultCount; - int taskNumber = tasks.stream().filter(task -> task.getTestCases().stream().anyMatch(tc -> tc.getTestName().equals(detail.testCaseName()))).findFirst() - .map(task -> tasks.stream().toList().indexOf(task) + 1).orElse(0); - return new FeedbackDetailDTO(detail.count(), relativeCount, detail.detailText(), detail.testCaseName(), taskNumber); + + // 2. Extract test case names using streams + List testCaseNames = programmingExercise.getTestCases().stream().map(ProgrammingExerciseTestCase::getTestName).toList(); + + List tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId); + + // 3. Generate filter task names directly + List filterTaskNames = data.getFilterTasks().stream().map(index -> { + int idx = Integer.parseInt(index); + return (idx > 0 && idx <= tasks.size()) ? tasks.get(idx - 1).getTaskName() : null; + }).filter(Objects::nonNull).toList(); + + // 4. Set minOccurrence and maxOccurrence based on filterOccurrence + long minOccurrence = data.getFilterOccurrence().length == 2 ? Long.parseLong(data.getFilterOccurrence()[0]) : 0; + long maxOccurrence = data.getFilterOccurrence().length == 2 ? Long.parseLong(data.getFilterOccurrence()[1]) : Integer.MAX_VALUE; + + // 5. Create pageable object for pagination + final var pageable = PageUtil.createDefaultPageRequest(data, PageUtil.ColumnMapping.FEEDBACK_ANALYSIS); + + // 6. Fetch filtered feedback from the repository + final Page feedbackDetailPage = studentParticipationRepository.findFilteredFeedbackByExerciseId(exerciseId, + StringUtils.isBlank(data.getSearchTerm()) ? "" : data.getSearchTerm().toLowerCase(), data.getFilterTestCases(), filterTaskNames, minOccurrence, maxOccurrence, + pageable); + + // 7. Process feedback details + // Map to index (+1 for 1-based indexing) + List processedDetails = feedbackDetailPage.getContent().stream().map(detail -> { + String taskIndex = tasks.stream().filter(task -> task.getTaskName().equals(detail.taskNumber())).findFirst().map(task -> String.valueOf(tasks.indexOf(task) + 1)) + .orElse("0"); + return new FeedbackDetailDTO(detail.count(), (detail.count() * 100.00) / distinctResultCount, detail.detailText(), detail.testCaseName(), taskIndex, "StudentError"); }).toList(); + + // 8. Return the response DTO containing feedback details, total elements, and test case/task info + return new FeedbackAnalysisResponseDTO(new SearchResultPageDTO<>(processedDetails, feedbackDetailPage.getTotalPages()), feedbackDetailPage.getTotalElements(), tasks.size(), + testCaseNames); + } + + /** + * Retrieves the maximum feedback count for a given exercise. + *
    + * This method calls the repository to fetch the maximum number of feedback occurrences across all feedback items for a specific exercise. + * This is used for filtering feedback based on the number of occurrences. + * + * @param exerciseId The ID of the exercise for which the maximum feedback count is to be retrieved. + * @return The maximum count of feedback occurrences for the given exercise. + */ + public long getMaxCountForExercise(long exerciseId) { + return studentParticipationRepository.findMaxCountForExercise(exerciseId); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java index 1692beaa7d69..5e28aa48b288 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java @@ -18,6 +18,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -27,19 +28,21 @@ import de.tum.cit.aet.artemis.assessment.domain.Feedback; import de.tum.cit.aet.artemis.assessment.domain.Result; -import de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO; +import de.tum.cit.aet.artemis.assessment.dto.FeedbackAnalysisResponseDTO; +import de.tum.cit.aet.artemis.assessment.dto.FeedbackPageableDTO; import de.tum.cit.aet.artemis.assessment.dto.ResultWithPointsPerGradingCriterionDTO; import de.tum.cit.aet.artemis.assessment.repository.ResultRepository; import de.tum.cit.aet.artemis.assessment.service.ResultService; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor; -import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastEditorInExercise; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastInstructorInExercise; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.core.util.HeaderUtil; import de.tum.cit.aet.artemis.exam.domain.Exam; @@ -280,16 +283,56 @@ public ResponseEntity createResultForExternalSubmission(@PathVariable Lo } /** - * GET /exercises/:exerciseId/feedback-details : Retrieves all aggregated feedback details for a given exercise. - * The feedback details include counts and relative counts of feedback occurrences, along with associated test case names and task numbers. + * GET /exercises/{exerciseId}/feedback-details : Retrieves paginated and filtered aggregated feedback details for a given exercise. + * The feedback details include counts and relative counts of feedback occurrences, test case names, and task numbers. + * The method allows filtering by a search term and sorting by various fields. + *
    + * Pagination is applied based on the provided query parameters, including page number, page size, sorting order, and search term. + * Sorting is applied by the specified sorted column and sorting order. If the provided sorted column is not valid for sorting (e.g., "taskNumber" or "errorCategory"), + * the sorting defaults to "count". + *
    + * Filtering is applied based on: + * - Task numbers (mapped to task names) + * - Test case names + * - Occurrence range (minimum and maximum occurrences) + *
    + * The response contains both the paginated feedback details and the total count of distinct results for the exercise. * * @param exerciseId The ID of the exercise for which feedback details should be retrieved. - * @return A ResponseEntity containing a list of {@link FeedbackDetailDTO}s + * @param data A {@link FeedbackPageableDTO} object containing pagination and filtering parameters, such as: + * - Page number + * - Page size + * - Search term (optional) + * - Sorting order (ASCENDING or DESCENDING) + * - Sorted column + * - Filter task numbers (optional) + * - Filter test case names (optional) + * - Occurrence range (optional) + * @return A {@link ResponseEntity} containing a {@link FeedbackAnalysisResponseDTO}, which includes: + * - {@link SearchResultPageDTO < FeedbackDetailDTO >} feedbackDetails: Paginated feedback details for the exercise. + * - long totalItems: The total number of feedback items (used for pagination). + * - int totalAmountOfTasks: The total number of tasks associated with the feedback. + * - List testCaseNames: A list of test case names included in the feedback. */ @GetMapping("exercises/{exerciseId}/feedback-details") - @EnforceAtLeastEditorInExercise - public ResponseEntity> getAllFeedbackDetailsForExercise(@PathVariable Long exerciseId) { - log.debug("REST request to get all Feedback details for Exercise {}", exerciseId); - return ResponseEntity.ok(resultService.findAggregatedFeedbackByExerciseId(exerciseId)); + @EnforceAtLeastInstructorInExercise + public ResponseEntity getFeedbackDetailsPaged(@PathVariable long exerciseId, @ModelAttribute FeedbackPageableDTO data) { + FeedbackAnalysisResponseDTO response = resultService.getFeedbackDetailsOnPage(exerciseId, data); + return ResponseEntity.ok(response); + } + + /** + * GET /exercises/{exerciseId}/feedback-details-max-count : Retrieves the maximum number of feedback occurrences for a given exercise. + * This method is useful for determining the highest count of feedback occurrences across all feedback items for the exercise, + * which can then be used to filter or adjust feedback analysis results. + * + * @param exerciseId The ID of the exercise for which the maximum feedback count should be retrieved. + * @return A {@link ResponseEntity} containing the maximum count of feedback occurrences (long). + */ + @GetMapping("exercises/{exerciseId}/feedback-details-max-count") + @EnforceAtLeastInstructorInExercise + public ResponseEntity getMaxCount(@PathVariable long exerciseId) { + long maxCount = resultService.getMaxCountForExercise(exerciseId); + return ResponseEntity.ok(maxCount); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/util/PageUtil.java b/src/main/java/de/tum/cit/aet/artemis/core/util/PageUtil.java index 40cbff0c217d..b1d3aaf5c20e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/util/PageUtil.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/util/PageUtil.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.JpaSort; import de.tum.cit.aet.artemis.core.dto.SortingOrder; import de.tum.cit.aet.artemis.core.dto.pageablesearch.PageableSearchDTO; @@ -69,6 +70,11 @@ public enum ColumnMapping { "id", "id", "name", "name", "build_completion_date", "buildCompletionDate" + )), + FEEDBACK_ANALYSIS(Map.of( + "count", "COUNT(f.id)", + "detailText", "f.detailText", + "testCaseName", "f.testCase.testName" )); // @formatter:on @@ -87,9 +93,29 @@ public String getMappedColumnName(String columnName) { } } + /** + * Creates a default {@link PageRequest} based on the provided {@link PageableSearchDTO} and {@link ColumnMapping}. + * This method maps the sorted column name from the provided search DTO using the column mapping, + * applies the appropriate sorting order (ascending or descending), and constructs a {@link PageRequest} + * with pagination and sorting information. + * + *

    + * If the mapped column name contains a "COUNT(" expression, this method treats it as an unsafe sort expression + * and uses {@link JpaSort(String)} to apply sorting directly to the database column. + *

    + * + * @param search The {@link PageableSearchDTO} containing pagination and sorting parameters (e.g., page number, page size, sorted column, and sorting order). + * @param columnMapping The {@link ColumnMapping} object used to map the sorted column name from the DTO to the actual database column. + * @return A {@link PageRequest} object containing the pagination and sorting options based on the search and column mapping. + * @throws IllegalArgumentException if any of the parameters are invalid or missing. + * @throws NullPointerException if the search or columnMapping parameters are null. + */ @NotNull public static PageRequest createDefaultPageRequest(PageableSearchDTO search, ColumnMapping columnMapping) { - var sortOptions = Sort.by(columnMapping.getMappedColumnName(search.getSortedColumn())); + String mappedColumn = columnMapping.getMappedColumnName(search.getSortedColumn()); + + var sortOptions = mappedColumn.contains("(") ? JpaSort.unsafe(mappedColumn) : Sort.by(mappedColumn); + sortOptions = search.getSortingOrder() == SortingOrder.ASCENDING ? sortOptions.ascending() : sortOptions.descending(); return PageRequest.of(search.getPage() - 1, search.getPageSize(), sortOptions); } diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java index aceb0bd9c2ae..499818ace8a2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/StudentParticipationRepository.java @@ -1199,12 +1199,29 @@ SELECT COALESCE(AVG(p.presentationScore), 0) double getAvgPresentationScoreByCourseId(@Param("courseId") long courseId); /** - * Retrieves aggregated feedback details for a given exercise, including the count of each unique feedback detail text and test case name. + * Retrieves aggregated feedback details for a given exercise, including the count of each unique feedback detail text, test case name, and task. *
    - * The relative count and task number are initially set to 0 and are calculated in a separate step in the service layer. + * The query calculates: + * - The number of occurrences of each feedback detail (COUNT). + * - The relative count as a percentage of the total distinct results. + * - The corresponding task name for each feedback item by checking if the feedback test case name is associated with a task. + *
    + * It supports filtering by: + * - Search term: Case-insensitive filtering on feedback detail text. + * - Test case names: Filters feedback based on specific test case names. + * - Task names: Filters feedback based on specific task names by mapping them to their associated test cases. + * - Occurrence range: Filters feedback based on the count of occurrences between the specified minimum and maximum values (inclusive). + *
    + * Grouping is done by feedback detail text and test case name. The occurrence count is filtered using the HAVING clause. * - * @param exerciseId Exercise ID. - * @return a list of {@link FeedbackDetailDTO} objects, with the relative count and task number set to 0. + * @param exerciseId The ID of the exercise for which feedback details should be retrieved. + * @param searchTerm The search term used for filtering the feedback detail text (optional). + * @param filterTestCases List of test case names to filter the feedback results (optional). + * @param filterTaskNames List of task names to filter feedback results based on the associated test cases (optional). + * @param minOccurrence The minimum number of occurrences to include in the results. + * @param maxOccurrence The maximum number of occurrences to include in the results. + * @param pageable Pagination information to apply. + * @return A page of {@link FeedbackDetailDTO} objects representing the aggregated feedback details. */ @Query(""" SELECT new de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO( @@ -1212,38 +1229,87 @@ SELECT COALESCE(AVG(p.presentationScore), 0) 0, f.detailText, f.testCase.testName, - 0 - ) + COALESCE(( + SELECT t.taskName + FROM ProgrammingExerciseTask t + JOIN t.testCases tct + WHERE t.exercise.id = :exerciseId AND tct.testName = f.testCase.testName + ), ''), + '' + ) FROM StudentParticipation p - JOIN p.results r + JOIN p.results r ON r.id = ( + SELECT MAX(pr.id) + FROM p.results pr + WHERE pr.participation.id = p.id + ) JOIN r.feedbacks f - WHERE p.exercise.id = :exerciseId - AND p.testRun = FALSE - AND r.id = ( - SELECT MAX(pr.id) - FROM p.results pr - ) - AND f.positive = FALSE - GROUP BY f.detailText, f.testCase.testName + WHERE p.exercise.id = :exerciseId + AND p.testRun = FALSE + AND f.positive = FALSE + AND (:searchTerm = '' OR LOWER(f.detailText) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(:searchTerm, '%', '\\%'), '_', '\\_'), '%')) ESCAPE '\\') + AND (:#{#filterTestCases != NULL && #filterTestCases.size() < 1} = TRUE OR f.testCase.testName IN (:filterTestCases)) + AND (:#{#filterTaskNames != NULL && #filterTaskNames.size() < 1} = TRUE OR f.testCase.testName IN ( + SELECT tct.testName + FROM ProgrammingExerciseTask t + JOIN t.testCases tct + WHERE t.taskName IN (:filterTaskNames) + )) + GROUP BY f.detailText, f.testCase.testName + HAVING COUNT(f.id) BETWEEN :minOccurrence AND :maxOccurrence """) - List findAggregatedFeedbackByExerciseId(@Param("exerciseId") long exerciseId); + Page findFilteredFeedbackByExerciseId(@Param("exerciseId") long exerciseId, @Param("searchTerm") String searchTerm, + @Param("filterTestCases") List filterTestCases, @Param("filterTaskNames") List filterTaskNames, @Param("minOccurrence") long minOccurrence, + @Param("maxOccurrence") long maxOccurrence, Pageable pageable); /** * Counts the distinct number of latest results for a given exercise, excluding those in practice mode. + *
    + * For each participation, it selects only the latest result (using MAX) and ensures that the participation is not a test run. * - * @param exerciseId Exercise ID. - * @return The count of distinct latest results for the exercise. + * @param exerciseId Exercise ID for which distinct results should be counted. + * @return The total number of distinct latest results for the given exercise. */ @Query(""" - SELECT COUNT(DISTINCT r.id) + SELECT COUNT(DISTINCT r.id) + FROM StudentParticipation p + JOIN p.results r ON r.id = ( + SELECT MAX(pr.id) + FROM p.results pr + WHERE pr.participation.id = p.id + ) + WHERE p.exercise.id = :exerciseId + AND p.testRun = FALSE + """) + long countDistinctResultsByExerciseId(@Param("exerciseId") long exerciseId); + + /** + * Retrieves the maximum feedback count for a given exercise. + *
    + * This query calculates the maximum number of feedback occurrences across all feedback entries for a specific exercise. + * It considers only the latest result per participation and excludes test runs. + *
    + * Grouping is done by feedback detail text and test case name, and the maximum feedback count is returned. + * + * @param exerciseId The ID of the exercise for which the maximum feedback count is to be retrieved. + * @return The maximum count of feedback occurrences for the given exercise. + */ + @Query(""" + SELECT MAX(feedbackCounts.feedbackCount) + FROM ( + SELECT COUNT(f.id) AS feedbackCount FROM StudentParticipation p - JOIN p.results r + JOIN p.results r ON r.id = ( + SELECT MAX(pr.id) + FROM p.results pr + WHERE pr.participation.id = p.id + ) + JOIN r.feedbacks f WHERE p.exercise.id = :exerciseId - AND p.testRun = FALSE - AND r.id = ( - SELECT MAX(pr.id) - FROM p.results pr - ) + AND p.testRun = FALSE + AND f.positive = FALSE + GROUP BY f.detailText, f.testCase.testName + ) AS feedbackCounts """) - long countDistinctResultsByExerciseId(@Param("exerciseId") long exerciseId); + long findMaxCountForExercise(@Param("exerciseId") long exerciseId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java index 579d714b18a8..5f2bb062c2ec 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java @@ -988,4 +988,15 @@ public String getFetchPath() { default ProgrammingExercise findByIdElseThrow(long programmingExerciseId) { return getValueElseThrow(findById(programmingExerciseId)); } + + /** + * Find a programming exercise by its id, including its test cases, and throw an Exception if it cannot be found. + * + * @param exerciseId of the programming exercise. + * @return The programming exercise with the associated test cases related to the given id. + * @throws EntityNotFoundException if the programming exercise with the given id cannot be found. + */ + default ProgrammingExercise findWithTestCasesByIdElseThrow(Long exerciseId) { + return getArbitraryValueElseThrow(findWithTestCasesById(exerciseId), Long.toString(exerciseId)); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ProgrammingExerciseTaskRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ProgrammingExerciseTaskRepository.java index 2c8db4544456..432727c61a3e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ProgrammingExerciseTaskRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/hestia/ProgrammingExerciseTaskRepository.java @@ -1,5 +1,6 @@ package de.tum.cit.aet.artemis.programming.repository.hestia; +import java.util.List; import java.util.Optional; import java.util.Set; @@ -54,7 +55,7 @@ default ProgrammingExerciseTask findByIdWithTestCaseAndSolutionEntriesElseThrow( * @throws EntityNotFoundException If the exercise with exerciseId does not exist */ @NotNull - default Set findByExerciseIdWithTestCaseAndSolutionEntriesElseThrow(long exerciseId) throws EntityNotFoundException { + default List findByExerciseIdWithTestCaseAndSolutionEntriesElseThrow(long exerciseId) throws EntityNotFoundException { return getArbitraryValueElseThrow(findByExerciseIdWithTestCaseAndSolutionEntries(exerciseId), Long.toString(exerciseId)); } @@ -72,7 +73,7 @@ default Set findByExerciseIdWithTestCaseAndSolutionEntr WHERE t.exercise.id = :exerciseId AND tc.exercise.id = :exerciseId """) - Optional> findByExerciseIdWithTestCaseAndSolutionEntries(@Param("exerciseId") long exerciseId); + Optional> findByExerciseIdWithTestCaseAndSolutionEntries(@Param("exerciseId") long exerciseId); /** * Gets all tasks with its test cases for a programming exercise diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java index d4e7de493f52..19f8e6e4cf80 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseService.java @@ -1004,7 +1004,7 @@ public boolean preCheckProjectExistsOnVCSOrCI(ProgrammingExercise programmingExe * @param exerciseId of the exercise */ public void deleteTasksWithSolutionEntries(Long exerciseId) { - Set tasks = programmingExerciseTaskRepository.findByExerciseIdWithTestCaseAndSolutionEntriesElseThrow(exerciseId); + List tasks = programmingExerciseTaskRepository.findByExerciseIdWithTestCaseAndSolutionEntriesElseThrow(exerciseId); Set solutionEntries = tasks.stream().map(ProgrammingExerciseTask::getTestCases).flatMap(Collection::stream) .map(ProgrammingExerciseTestCase::getSolutionEntries).flatMap(Collection::stream).collect(Collectors.toSet()); programmingExerciseTaskRepository.deleteAll(tasks); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/hestia/ProgrammingExerciseTaskService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/hestia/ProgrammingExerciseTaskService.java index 24ed52858fe1..1684cf52c018 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/hestia/ProgrammingExerciseTaskService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/hestia/ProgrammingExerciseTaskService.java @@ -189,8 +189,8 @@ public Set getTasksWithoutInactiveTestCases(long exerci * @param exerciseId of the programming exercise * @return Set of all tasks including one for not manually assigned tests */ - public Set getTasksWithUnassignedTestCases(long exerciseId) { - Set tasks = programmingExerciseTaskRepository.findByExerciseIdWithTestCaseAndSolutionEntriesElseThrow(exerciseId); + public List getTasksWithUnassignedTestCases(long exerciseId) { + List tasks = programmingExerciseTaskRepository.findByExerciseIdWithTestCaseAndSolutionEntriesElseThrow(exerciseId); Set testsWithTasks = tasks.stream().flatMap(task -> task.getTestCases().stream()).collect(Collectors.toSet()); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/hestia/ProgrammingExerciseTaskResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/hestia/ProgrammingExerciseTaskResource.java index 8618c9be7c1c..379ebfacb035 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/hestia/ProgrammingExerciseTaskResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/hestia/ProgrammingExerciseTaskResource.java @@ -2,6 +2,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import java.util.List; import java.util.Set; import org.slf4j.Logger; @@ -74,13 +75,13 @@ public ResponseEntity> getTasks(@PathVariable Long */ @GetMapping("programming-exercises/{exerciseId}/tasks-with-unassigned-test-cases") @EnforceAtLeastTutor - public ResponseEntity> getTasksWithUnassignedTask(@PathVariable Long exerciseId) { + public ResponseEntity> getTasksWithUnassignedTask(@PathVariable Long exerciseId) { log.debug("REST request to retrieve ProgrammingExerciseTasks for ProgrammingExercise with id : {}", exerciseId); // Reload the exercise from the database as we can't trust data from the client ProgrammingExercise exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.TEACHING_ASSISTANT, exercise, null); - Set tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId); + List tasks = programmingExerciseTaskService.getTasksWithUnassignedTestCases(exerciseId); return ResponseEntity.ok(tasks); } } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.html b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.html new file mode 100644 index 000000000000..0571f1039ca6 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.html @@ -0,0 +1,70 @@ + + + + diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.ts new file mode 100644 index 000000000000..09e6784658b9 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component.ts @@ -0,0 +1,90 @@ +import { Component, computed, inject, output, signal } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { RangeSliderComponent } from 'app/shared/range-slider/range-slider.component'; +import { FeedbackAnalysisService } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { LocalStorageService } from 'ngx-webstorage'; + +export interface FilterData { + tasks: string[]; + testCases: string[]; + occurrence: number[]; +} + +@Component({ + selector: 'jhi-feedback-filter-modal', + templateUrl: './feedback-filter-modal.component.html', + imports: [RangeSliderComponent, ArtemisSharedCommonModule], + providers: [FeedbackAnalysisService], + standalone: true, +}) +export class FeedbackFilterModalComponent { + private localStorage = inject(LocalStorageService); + private activeModal = inject(NgbActiveModal); + + filterApplied = output(); + + readonly FILTER_TASKS_KEY = 'feedbackAnalysis.tasks'; + readonly FILTER_TEST_CASES_KEY = 'feedbackAnalysis.testCases'; + readonly FILTER_OCCURRENCE_KEY = 'feedbackAnalysis.occurrence'; + + readonly totalAmountOfTasks = signal(0); + readonly testCaseNames = signal([]); + readonly minCount = signal(0); + readonly maxCount = signal(0); + readonly taskArray = computed(() => Array.from({ length: this.totalAmountOfTasks() }, (_, i) => i + 1)); + + filters: FilterData = { + tasks: [], + testCases: [], + occurrence: [this.minCount(), this.maxCount() || 1], + }; + + applyFilter(): void { + this.localStorage.store(this.FILTER_TASKS_KEY, this.filters.tasks); + this.localStorage.store(this.FILTER_TEST_CASES_KEY, this.filters.testCases); + this.localStorage.store(this.FILTER_OCCURRENCE_KEY, this.filters.occurrence); + this.filterApplied.emit(this.filters); + this.activeModal.close(); + } + + clearFilter(): void { + this.localStorage.clear(this.FILTER_TASKS_KEY); + this.localStorage.clear(this.FILTER_TEST_CASES_KEY); + this.localStorage.clear(this.FILTER_OCCURRENCE_KEY); + this.filters = { + tasks: [], + testCases: [], + occurrence: [this.minCount(), this.maxCount()], + }; + this.filterApplied.emit(this.filters); + this.activeModal.close(); + } + + onCheckboxChange(event: Event, controlName: keyof FilterData): void { + const checkbox = event.target as HTMLInputElement; + const values = this.filters[controlName]; + + if (controlName === 'occurrence') { + const numericValue = Number(checkbox.value); + this.pushValue(checkbox, values as number[], numericValue); + } else { + this.pushValue(checkbox, values as string[], checkbox.value); + } + } + + private pushValue(checkbox: HTMLInputElement, values: T[], valueToAddOrRemove: T): void { + if (checkbox.checked) { + values.push(valueToAddOrRemove); + } else { + const index = values.indexOf(valueToAddOrRemove); + if (index >= 0) { + values.splice(index, 1); + } + } + } + + closeModal(): void { + this.activeModal.dismiss(); + } +} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html new file mode 100644 index 000000000000..abf8a2bf5b47 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.html @@ -0,0 +1,44 @@ +
    +

    + +
    +
    +
    +
    + + + + + + + + +
    + +
    +

    {{ feedbackDetail().detailText }}

    +
    + + +
    + +

    {{ value }}

    +
    +
    diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.ts new file mode 100644 index 000000000000..7178244be5d2 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component.ts @@ -0,0 +1,15 @@ +import { Component, inject, input } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; + +@Component({ + selector: 'jhi-feedback-modal', + templateUrl: './feedback-modal.component.html', + imports: [ArtemisSharedCommonModule], + standalone: true, +}) +export class FeedbackModalComponent { + feedbackDetail = input.required(); + activeModal = inject(NgbActiveModal); +} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.html b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.html index 4c76747e8e96..295cb206a373 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.html +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.html @@ -1,27 +1,81 @@ + + + + @if (sortedColumn() === column) { + + } + +
    -

    +
    +

    +
    + + +
    +
    - - - - - + + + + + - @for (item of feedbackDetails; track item) { + @for (item of content().resultsOnPage; track item) { - + - - + + }
    {{ item.count }} ({{ item.relativeCount | number: '1.0-0' }}%){{ item.detailText }} + {{ item.detailText.length > MAX_FEEDBACK_DETAIL_TEXT_LENGTH ? (item.detailText | slice: 0 : 100) + '...' : item.detailText }} + {{ item.taskNumber }} {{ item.testCaseName }}Student Error{{ item.errorCategory }} + +
    -
    +
    + + +
    + +
    +
    diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.scss b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.scss new file mode 100644 index 000000000000..64756aa92d8b --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.scss @@ -0,0 +1,9 @@ +.position-relative { + position: relative; +} + +.search-icon { + position: absolute; + right: 10px; + pointer-events: none; +} diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts index 7e1d48121f1c..24855955f5b7 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component.ts @@ -1,34 +1,170 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { Component, computed, effect, inject, input, signal, untracked } from '@angular/core'; +import { FeedbackAnalysisService, FeedbackDetail } from './feedback-analysis.service'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { AlertService } from 'app/core/util/alert.service'; +import { faFilter, faSort, faSortDown, faSortUp, faUpRightAndDownLeftFromCenter } from '@fortawesome/free-solid-svg-icons'; +import { SearchResult, SortingOrder } from 'app/shared/table/pageable-table'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { FeedbackModalComponent } from 'app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component'; +import { FeedbackFilterModalComponent, FilterData } from 'app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component'; +import { LocalStorageService } from 'ngx-webstorage'; +import { BaseApiHttpService } from 'app/course/learning-paths/services/base-api-http.service'; @Component({ selector: 'jhi-feedback-analysis', templateUrl: './feedback-analysis.component.html', + styleUrls: ['./feedback-analysis.component.scss'], standalone: true, - imports: [ArtemisSharedModule], + imports: [ArtemisSharedCommonModule], providers: [FeedbackAnalysisService], }) -export class FeedbackAnalysisComponent implements OnInit { - @Input() exerciseTitle: string; - @Input() exerciseId: number; - feedbackDetails: FeedbackDetail[] = []; +export class FeedbackAnalysisComponent { + exerciseTitle = input.required(); + exerciseId = input.required(); - constructor( - private feedbackAnalysisService: FeedbackAnalysisService, - private alertService: AlertService, - ) {} + private feedbackAnalysisService = inject(FeedbackAnalysisService); + private alertService = inject(AlertService); + private modalService = inject(NgbModal); + private localStorage = inject(LocalStorageService); - ngOnInit(): void { - this.loadFeedbackDetails(this.exerciseId); + readonly page = signal(1); + readonly pageSize = signal(20); + readonly searchTerm = signal(''); + readonly sortingOrder = signal(SortingOrder.DESCENDING); + readonly sortedColumn = signal('count'); + + readonly content = signal>({ resultsOnPage: [], numberOfPages: 0 }); + readonly totalItems = signal(0); + readonly collectionsSize = computed(() => this.content().numberOfPages * this.pageSize()); + + readonly faSort = faSort; + readonly faSortUp = faSortUp; + readonly faSortDown = faSortDown; + readonly faFilter = faFilter; + readonly faUpRightAndDownLeftFromCenter = faUpRightAndDownLeftFromCenter; + readonly SortingOrder = SortingOrder; + readonly MAX_FEEDBACK_DETAIL_TEXT_LENGTH = 150; + readonly sortIcon = computed(() => (this.sortingOrder() === SortingOrder.ASCENDING ? this.faSortUp : this.faSortDown)); + + readonly FILTER_TASKS_KEY = 'feedbackAnalysis.tasks'; + readonly FILTER_TEST_CASES_KEY = 'feedbackAnalysis.testCases'; + readonly FILTER_OCCURRENCE_KEY = 'feedbackAnalysis.occurrence'; + readonly selectedFiltersCount = signal(0); + readonly totalAmountOfTasks = signal(0); + readonly testCaseNames = signal([]); + readonly minCount = signal(0); + readonly maxCount = signal(0); + + private readonly debounceLoadData = BaseApiHttpService.debounce(this.loadData.bind(this), 300); + + constructor() { + effect(() => { + untracked(async () => { + await this.loadData(); + }); + }); } - async loadFeedbackDetails(exerciseId: number): Promise { + private async loadData(): Promise { + const savedTasks = this.localStorage.retrieve(this.FILTER_TASKS_KEY) || []; + const savedTestCases = this.localStorage.retrieve(this.FILTER_TEST_CASES_KEY) || []; + const savedOccurrence = this.localStorage.retrieve(this.FILTER_OCCURRENCE_KEY) || []; + + const state = { + page: this.page(), + pageSize: this.pageSize(), + searchTerm: this.searchTerm() || '', + sortingOrder: this.sortingOrder(), + sortedColumn: this.sortedColumn(), + }; + try { - this.feedbackDetails = await this.feedbackAnalysisService.getFeedbackDetailsForExercise(exerciseId); + const response = await this.feedbackAnalysisService.search(state, { + exerciseId: this.exerciseId(), + filters: { + tasks: this.selectedFiltersCount() !== 0 ? savedTasks : [], + testCases: this.selectedFiltersCount() !== 0 ? savedTestCases : [], + occurrence: this.selectedFiltersCount() !== 0 ? savedOccurrence : [], + }, + }); + this.content.set(response.feedbackDetails); + this.totalItems.set(response.totalItems); + this.totalAmountOfTasks.set(response.totalAmountOfTasks); + this.testCaseNames.set(response.testCaseNames); } catch (error) { - this.alertService.error(`artemisApp.programmingExercise.configureGrading.feedbackAnalysis.error`); + this.alertService.error('artemisApp.programmingExercise.configureGrading.feedbackAnalysis.error'); + } + } + + setPage(newPage: number): void { + this.page.set(newPage); + this.loadData(); + } + + async search(searchTerm: string): Promise { + this.page.set(1); + this.searchTerm.set(searchTerm); + this.debounceLoadData(); + } + + openFeedbackModal(feedbackDetail: FeedbackDetail): void { + const modalRef = this.modalService.open(FeedbackModalComponent, { centered: true }); + modalRef.componentInstance.feedbackDetail = signal(feedbackDetail); + } + + isSortableColumn(column: string): boolean { + return ['count', 'detailText', 'testCaseName'].includes(column); + } + + setSortedColumn(column: string): void { + if (this.sortedColumn() === column) { + this.sortingOrder.set(this.sortingOrder() === SortingOrder.ASCENDING ? SortingOrder.DESCENDING : SortingOrder.ASCENDING); + } else { + this.sortedColumn.set(column); + this.sortingOrder.set(SortingOrder.ASCENDING); + } + this.loadData(); + } + + async openFilterModal(): Promise { + const savedTasks = this.localStorage.retrieve(this.FILTER_TASKS_KEY); + const savedTestCases = this.localStorage.retrieve(this.FILTER_TEST_CASES_KEY); + const savedOccurrence = this.localStorage.retrieve(this.FILTER_OCCURRENCE_KEY); + this.minCount.set(0); + this.maxCount.set(await this.feedbackAnalysisService.getMaxCount(this.exerciseId())); + + const modalRef = this.modalService.open(FeedbackFilterModalComponent, { centered: true, size: 'lg' }); + + modalRef.componentInstance.exerciseId = this.exerciseId; + modalRef.componentInstance.totalAmountOfTasks = this.totalAmountOfTasks; + modalRef.componentInstance.testCaseNames = this.testCaseNames; + modalRef.componentInstance.maxCount = this.maxCount; + modalRef.componentInstance.filters = { + tasks: this.selectedFiltersCount() !== 0 ? savedTasks : [], + testCases: this.selectedFiltersCount() !== 0 ? savedTestCases : [], + occurrence: this.selectedFiltersCount() !== 0 ? savedOccurrence : [this.minCount(), this.maxCount()], + }; + modalRef.componentInstance.filterApplied.subscribe((filters: any) => { + this.applyFilters(filters); + }); + } + + applyFilters(filters: FilterData): void { + this.selectedFiltersCount.set(this.countAppliedFilters(filters)); + this.loadData(); + } + + countAppliedFilters(filters: FilterData): number { + let count = 0; + if (filters.tasks && filters.tasks.length > 0) { + count += filters.tasks.length; + } + if (filters.testCases && filters.testCases.length > 0) { + count += filters.testCases.length; + } + if (filters.occurrence && (filters.occurrence[0] !== 0 || filters.occurrence[1] !== this.maxCount())) { + count++; } + return count; } } diff --git a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service.ts b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service.ts index 4fa81cf289d3..bb235de5a24c 100644 --- a/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service.ts @@ -1,19 +1,40 @@ import { Injectable } from '@angular/core'; +import { SearchResult, SearchTermPageableSearch } from 'app/shared/table/pageable-table'; import { BaseApiHttpService } from 'app/course/learning-paths/services/base-api-http.service'; +import { HttpParams } from '@angular/common/http'; +import { FilterData } from 'app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component'; +export interface FeedbackAnalysisResponse { + feedbackDetails: SearchResult; + totalItems: number; + totalAmountOfTasks: number; + testCaseNames: string[]; +} export interface FeedbackDetail { count: number; relativeCount: number; detailText: string; testCaseName: string; - taskNumber: number; + taskNumber: string; + errorCategory: string; } - @Injectable() export class FeedbackAnalysisService extends BaseApiHttpService { - private readonly EXERCISE_RESOURCE_URL = 'exercises'; + search(pageable: SearchTermPageableSearch, options: { exerciseId: number; filters: FilterData }): Promise { + const params = new HttpParams() + .set('page', pageable.page.toString()) + .set('pageSize', pageable.pageSize.toString()) + .set('searchTerm', pageable.searchTerm || '') + .set('sortingOrder', pageable.sortingOrder) + .set('sortedColumn', pageable.sortedColumn) + .set('filterTasks', options.filters.tasks.join(',')) + .set('filterTestCases', options.filters.testCases.join(',')) + .set('filterOccurrence', options.filters.occurrence.join(',')); + + return this.get(`exercises/${options.exerciseId}/feedback-details`, { params }); + } - getFeedbackDetailsForExercise(exerciseId: number): Promise { - return this.get(`${this.EXERCISE_RESOURCE_URL}/${exerciseId}/feedback-details`); + getMaxCount(exerciseId: number): Promise { + return this.get(`exercises/${exerciseId}/feedback-details-max-count`); } } diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index e3a86ea5c71d..f26d5955a9ef 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -351,7 +351,23 @@ "testcase": "Testfall", "errorCategory": "Fehlerkategorie", "totalItems": "Insgesamt {{count}} Elemente", - "error": "Beim Laden des Feedbacks ist ein Fehler aufgetreten." + "error": "Beim Laden des Feedbacks ist ein Fehler aufgetreten.", + "search": "Suche ...", + "filter": "Filter", + "feedbackModal": { + "header": "Fehlerdetails", + "feedbackTitle": "Feedback zu Testfällen", + "ok": "Ok" + }, + "filterModal": { + "modalTitle": "Filteroptionen", + "task": "Tasks", + "testcase": "Testfälle", + "occurrence": "Häufigkeit", + "clear": "Filter zurücksetzen", + "cancel": "Abbrechen", + "apply": "Filter anwenden" + } }, "help": { "name": "Aufgabennamen werden fett geschrieben, während Testnamen normal sind. Ob es ein Aufgabenname oder Testname ist hängt davon ab, ob die Reihe eine Aufgabe oder einen Test darstellt.", diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index 6929f51511f7..7659f48e78b2 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -351,7 +351,23 @@ "testcase": "Test Case", "errorCategory": "Error Category", "totalItems": "In total {{count}} items", - "error": "An error occurred while loading the feedback." + "error": "An error occurred while loading the feedback.", + "search": "Search ...", + "filter": "Filters", + "feedbackModal": { + "header": "Error Details", + "feedbackTitle": "Test Case Feedback", + "ok": "Ok" + }, + "filterModal": { + "modalTitle": "Filter Options", + "task": "Tasks", + "testcase": "Test Cases", + "occurrence": "Occurrence", + "clear": "Clear Filter", + "cancel": "Cancel", + "apply": "Apply Filter" + } }, "help": { "name": "Task names are written in bold whereas Test names are normal. Task or test name depending on whether the row is a task or test.", diff --git a/src/test/java/de/tum/cit/aet/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/assessment/ResultServiceIntegrationTest.java index c9ce9e923042..1d04c504db90 100644 --- a/src/test/java/de/tum/cit/aet/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/assessment/ResultServiceIntegrationTest.java @@ -30,6 +30,7 @@ import de.tum.cit.aet.artemis.assessment.domain.GradingCriterion; import de.tum.cit.aet.artemis.assessment.domain.GradingInstruction; import de.tum.cit.aet.artemis.assessment.domain.Result; +import de.tum.cit.aet.artemis.assessment.dto.FeedbackAnalysisResponseDTO; import de.tum.cit.aet.artemis.assessment.dto.FeedbackDetailDTO; import de.tum.cit.aet.artemis.assessment.dto.ResultWithPointsPerGradingCriterionDTO; import de.tum.cit.aet.artemis.assessment.repository.FeedbackRepository; @@ -729,8 +730,6 @@ void testGetAssessmentCountByCorrectionRoundForProgrammingExercise() { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetAllFeedbackDetailsForExercise() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); - StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); - Result result = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation); ProgrammingExerciseTestCase testCase = programmingExerciseUtilService.addTestCaseToProgrammingExercise(programmingExercise, "test1"); testCase.setId(1L); @@ -738,26 +737,35 @@ void testGetAllFeedbackDetailsForExercise() throws Exception { feedback.setPositive(false); feedback.setDetailText("Some feedback"); feedback.setTestCase(testCase); + + StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); + + Result result = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation); + participationUtilService.addFeedbackToResult(feedback, result); - List response = request.getList("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, FeedbackDetailDTO.class); + String url = "/api/exercises/" + programmingExercise.getId() + "/feedback-details" + "?page=1&pageSize=10&sortedColumn=detailText&sortingOrder=ASCENDING" + + "&searchTerm=&filterTasks=&filterTestCases=&filterOccurrence="; + + FeedbackAnalysisResponseDTO response = request.get(url, HttpStatus.OK, FeedbackAnalysisResponseDTO.class); - assertThat(response).isNotEmpty(); - FeedbackDetailDTO feedbackDetail = response.getFirst(); + assertThat(response.feedbackDetails().getResultsOnPage()).isNotEmpty(); + FeedbackDetailDTO feedbackDetail = response.feedbackDetails().getResultsOnPage().getFirst(); assertThat(feedbackDetail.count()).isEqualTo(1); assertThat(feedbackDetail.relativeCount()).isEqualTo(100.0); assertThat(feedbackDetail.detailText()).isEqualTo("Some feedback"); assertThat(feedbackDetail.testCaseName()).isEqualTo("test1"); - assertThat(feedbackDetail.taskNumber()).isEqualTo(1); + + assertThat(response.totalItems()).isEqualTo(1); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetAllFeedbackDetailsForExerciseWithMultipleFeedback() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); - StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); + StudentParticipation participation1 = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); StudentParticipation participation2 = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student2"); - Result result = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation); + Result result1 = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation1); Result result2 = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation2); ProgrammingExerciseTestCase testCase = programmingExerciseUtilService.addTestCaseToProgrammingExercise(programmingExercise, "test1"); testCase.setId(1L); @@ -766,7 +774,7 @@ void testGetAllFeedbackDetailsForExerciseWithMultipleFeedback() throws Exception feedback1.setPositive(false); feedback1.setDetailText("Some feedback"); feedback1.setTestCase(testCase); - participationUtilService.addFeedbackToResult(feedback1, result); + participationUtilService.addFeedbackToResult(feedback1, result1); Feedback feedback2 = new Feedback(); feedback2.setPositive(false); @@ -778,37 +786,79 @@ void testGetAllFeedbackDetailsForExerciseWithMultipleFeedback() throws Exception feedback3.setPositive(false); feedback3.setDetailText("Some different feedback"); feedback3.setTestCase(testCase); - participationUtilService.addFeedbackToResult(feedback3, result); + participationUtilService.addFeedbackToResult(feedback3, result1); + + String url = "/api/exercises/" + programmingExercise.getId() + "/feedback-details" + "?page=1&pageSize=10&sortedColumn=detailText&sortingOrder=ASCENDING" + + "&searchTerm=&filterTasks=&filterTestCases=&filterOccurrence="; - List response = request.getList("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, FeedbackDetailDTO.class); + FeedbackAnalysisResponseDTO response = request.get(url, HttpStatus.OK, FeedbackAnalysisResponseDTO.class); - assertThat(response).hasSize(2); + List feedbackDetails = response.feedbackDetails().getResultsOnPage(); + assertThat(feedbackDetails).hasSize(2); - FeedbackDetailDTO firstFeedbackDetail = response.stream().filter(feedbackDetail -> "Some feedback".equals(feedbackDetail.detailText())).findFirst().orElseThrow(); + FeedbackDetailDTO firstFeedbackDetail = feedbackDetails.stream().filter(feedbackDetail -> "Some feedback".equals(feedbackDetail.detailText())).findFirst().orElseThrow(); - FeedbackDetailDTO secondFeedbackDetail = response.stream().filter(feedbackDetail -> "Some different feedback".equals(feedbackDetail.detailText())).findFirst() + FeedbackDetailDTO secondFeedbackDetail = feedbackDetails.stream().filter(feedbackDetail -> "Some different feedback".equals(feedbackDetail.detailText())).findFirst() .orElseThrow(); assertThat(firstFeedbackDetail.count()).isEqualTo(2); assertThat(firstFeedbackDetail.relativeCount()).isEqualTo(100.0); assertThat(firstFeedbackDetail.detailText()).isEqualTo("Some feedback"); assertThat(firstFeedbackDetail.testCaseName()).isEqualTo("test1"); - assertThat(firstFeedbackDetail.taskNumber()).isEqualTo(1); assertThat(secondFeedbackDetail.count()).isEqualTo(1); assertThat(secondFeedbackDetail.relativeCount()).isEqualTo(50.0); assertThat(secondFeedbackDetail.detailText()).isEqualTo("Some different feedback"); assertThat(secondFeedbackDetail.testCaseName()).isEqualTo("test1"); - assertThat(secondFeedbackDetail.taskNumber()).isEqualTo(1); + + assertThat(response.totalItems()).isEqualTo(2); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testGetAllFeedbackDetailsForExercise_NoParticipation() throws Exception { + void testGetMaxCountForExercise() throws Exception { ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); - List response = request.getList("/api/exercises/" + programmingExercise.getId() + "/feedback-details", HttpStatus.OK, FeedbackDetailDTO.class); + StudentParticipation participation = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); + Result result = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation); + ProgrammingExerciseTestCase testCase = programmingExerciseUtilService.addTestCaseToProgrammingExercise(programmingExercise, "test1"); + testCase.setId(1L); + + Feedback feedback = new Feedback(); + feedback.setPositive(false); + feedback.setDetailText("Some feedback"); + feedback.setTestCase(testCase); + participationUtilService.addFeedbackToResult(feedback, result); - assertThat(response).isEmpty(); + long maxCount = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details-max-count", HttpStatus.OK, Long.class); + + assertThat(maxCount).isEqualTo(1); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetMaxCountForExerciseWithMultipleFeedback() throws Exception { + ProgrammingExercise programmingExercise = programmingExerciseUtilService.addProgrammingExerciseToCourse(course); + StudentParticipation participation1 = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student1"); + StudentParticipation participation2 = participationUtilService.createAndSaveParticipationForExercise(programmingExercise, TEST_PREFIX + "student2"); + Result result1 = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation1); + Result result2 = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, null, participation2); + ProgrammingExerciseTestCase testCase = programmingExerciseUtilService.addTestCaseToProgrammingExercise(programmingExercise, "test1"); + testCase.setId(1L); + + Feedback feedback1 = new Feedback(); + feedback1.setPositive(false); + feedback1.setDetailText("Some feedback"); + feedback1.setTestCase(testCase); + participationUtilService.addFeedbackToResult(feedback1, result1); + + Feedback feedback2 = new Feedback(); + feedback2.setPositive(false); + feedback2.setDetailText("Some feedback"); + feedback2.setTestCase(testCase); + participationUtilService.addFeedbackToResult(feedback2, result2); + + long maxCount = request.get("/api/exercises/" + programmingExercise.getId() + "/feedback-details-max-count", HttpStatus.OK, Long.class); + + assertThat(maxCount).isEqualTo(2); + } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseTaskIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseTaskIntegrationTest.java index c4def2a98eb6..55f7d322a023 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseTaskIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseTaskIntegrationTest.java @@ -4,6 +4,7 @@ import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -114,7 +115,7 @@ void testTaskExtractionForProgrammingExercise() throws Exception { programmingExerciseTaskService.updateTasksFromProblemStatement(programmingExercise); request.get("/api/programming-exercises/" + programmingExercise.getId() + "/tasks", HttpStatus.OK, Set.class); - Set extractedTasks = taskRepository.findByExerciseIdWithTestCaseAndSolutionEntriesElseThrow(programmingExercise.getId()); + List extractedTasks = taskRepository.findByExerciseIdWithTestCaseAndSolutionEntriesElseThrow(programmingExercise.getId()); Optional task1Optional = extractedTasks.stream().filter(task -> task.getTaskName().equals(taskName1)).findFirst(); Optional task2Optional = extractedTasks.stream().filter(task -> task.getTaskName().equals(taskName2)).findFirst(); assertThat(task1Optional).isPresent(); diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts index 0e9387b93e5b..00ab084dd543 100644 --- a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.component.spec.ts @@ -3,63 +3,214 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; import { ArtemisTestModule } from '../../../test.module'; import { FeedbackAnalysisComponent } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.component'; -import { FeedbackAnalysisService } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; -import { FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; +import { FeedbackAnalysisResponse, FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { LocalStorageService } from 'ngx-webstorage'; +import '@angular/localize/init'; +import { FeedbackFilterModalComponent } from 'app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component'; describe('FeedbackAnalysisComponent', () => { let fixture: ComponentFixture; let component: FeedbackAnalysisComponent; let feedbackAnalysisService: FeedbackAnalysisService; - let getFeedbackDetailsSpy: jest.SpyInstance; + let searchSpy: jest.SpyInstance; + let localStorageService: LocalStorageService; const feedbackMock: FeedbackDetail[] = [ - { detailText: 'Test feedback 1 detail', testCaseName: 'test1', count: 10, relativeCount: 50, taskNumber: 1 }, - { detailText: 'Test feedback 2 detail', testCaseName: 'test2', count: 5, relativeCount: 25, taskNumber: 2 }, + { detailText: 'Test feedback 1 detail', testCaseName: 'test1', count: 10, relativeCount: 50, taskNumber: '1', errorCategory: 'StudentError' }, + { detailText: 'Test feedback 2 detail', testCaseName: 'test2', count: 5, relativeCount: 25, taskNumber: '2', errorCategory: 'StudentError' }, ]; + const feedbackResponseMock: FeedbackAnalysisResponse = { + feedbackDetails: { resultsOnPage: feedbackMock, numberOfPages: 1 }, + totalItems: 2, + totalAmountOfTasks: 1, + testCaseNames: ['test1', 'test2'], + }; + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ArtemisTestModule, TranslateModule.forRoot(), FeedbackAnalysisComponent], - declarations: [], providers: [ { provide: TranslateService, useClass: MockTranslateService, }, FeedbackAnalysisService, + LocalStorageService, ], }).compileComponents(); + fixture = TestBed.createComponent(FeedbackAnalysisComponent); component = fixture.componentInstance; - component.exerciseId = 1; feedbackAnalysisService = fixture.debugElement.injector.get(FeedbackAnalysisService); - getFeedbackDetailsSpy = jest.spyOn(feedbackAnalysisService, 'getFeedbackDetailsForExercise').mockResolvedValue(feedbackMock); + localStorageService = fixture.debugElement.injector.get(LocalStorageService); + + jest.spyOn(localStorageService, 'retrieve').mockReturnValue([]); + + searchSpy = jest.spyOn(feedbackAnalysisService, 'search').mockResolvedValue(feedbackResponseMock); + + fixture.componentRef.setInput('exerciseId', 1); + fixture.componentRef.setInput('exerciseTitle', 'Sample Exercise Title'); + + fixture.detectChanges(); }); - describe('ngOnInit', () => { - it('should call loadFeedbackDetails when exerciseId is provided', async () => { - component.ngOnInit(); - await fixture.whenStable(); + afterEach(() => { + jest.restoreAllMocks(); + }); - expect(getFeedbackDetailsSpy).toHaveBeenCalledWith(1); - expect(component.feedbackDetails).toEqual(feedbackMock); + describe('on init', () => { + it('should load data on initialization', async () => { + await fixture.whenStable(); + expect(searchSpy).toHaveBeenCalledOnce(); + expect(component.content().resultsOnPage).toEqual(feedbackMock); + expect(component.totalItems()).toBe(2); }); }); - describe('loadFeedbackDetails', () => { - it('should load feedback details and update the component state', async () => { - await component.loadFeedbackDetails(1); - expect(component.feedbackDetails).toEqual(feedbackMock); + describe('loadData', () => { + it('should load feedback details and update state correctly', async () => { + await component['loadData'](); + expect(searchSpy).toHaveBeenCalledTimes(2); + expect(component.content().resultsOnPage).toEqual(feedbackMock); + expect(component.totalItems()).toBe(2); }); it('should handle error while loading feedback details', async () => { - getFeedbackDetailsSpy.mockRejectedValue(new Error('Error loading feedback details')); + searchSpy.mockRejectedValueOnce(new Error('Error loading feedback details')); try { - await component.loadFeedbackDetails(1); + await component['loadData'](); } catch { - expect(component.feedbackDetails).toEqual([]); + expect(component.content().resultsOnPage).toEqual([]); + expect(component.totalItems()).toBe(0); } }); }); + + describe('setPage', () => { + it('should update page and reload data', async () => { + const loadDataSpy = jest.spyOn(component, 'loadData' as any); + + component.setPage(2); + expect(component.page()).toBe(2); + expect(loadDataSpy).toHaveBeenCalledOnce(); + }); + }); + + describe('setSortedColumn', () => { + it('should update sortedColumn and sortingOrder, and reload data', async () => { + const loadDataSpy = jest.spyOn(component, 'loadData' as any); + + component.setSortedColumn('testCaseName'); + expect(component.sortedColumn()).toBe('testCaseName'); + expect(component.sortingOrder()).toBe('ASCENDING'); + expect(loadDataSpy).toHaveBeenCalledOnce(); + + component.setSortedColumn('testCaseName'); + expect(component.sortingOrder()).toBe('DESCENDING'); + expect(loadDataSpy).toHaveBeenCalledTimes(2); + }); + }); + + describe('search', () => { + beforeEach(() => { + jest.spyOn(component, 'debounceLoadData' as any).mockImplementation(() => { + component['loadData'](); + }); + }); + + it('should reset page and load data when searching', async () => { + const loadDataSpy = jest.spyOn(component, 'loadData' as any); + component.searchTerm.set('test'); + await component.search(component.searchTerm()); + expect(component.page()).toBe(1); + expect(loadDataSpy).toHaveBeenCalledOnce(); + }); + }); + + describe('openFeedbackModal', () => { + it('should open feedback modal with correct feedback detail', () => { + const modalService = fixture.debugElement.injector.get(NgbModal); + const modalSpy = jest.spyOn(modalService, 'open').mockReturnValue({ componentInstance: {} } as any); + + const feedbackDetail = feedbackMock[0]; + component.openFeedbackModal(feedbackDetail); + + expect(modalSpy).toHaveBeenCalledOnce(); + }); + }); + + describe('openFilterModal', () => { + it('should open filter modal and pass correct form values and properties', async () => { + const modalService = fixture.debugElement.injector.get(NgbModal); + const modalSpy = jest.spyOn(modalService, 'open').mockReturnValue({ + componentInstance: { + filterApplied: { subscribe: jest.fn() }, + }, + } as any); + const getMaxCountSpy = jest.spyOn(feedbackAnalysisService, 'getMaxCount').mockResolvedValue(10); + jest.spyOn(localStorageService, 'retrieve').mockReturnValueOnce(['task1']).mockReturnValueOnce(['testCase1']).mockReturnValueOnce([component.minCount(), 5]); + + component.maxCount.set(5); + component.selectedFiltersCount.set(1); + await component.openFilterModal(); + + expect(getMaxCountSpy).toHaveBeenCalledWith(1); + expect(modalSpy).toHaveBeenCalledWith(FeedbackFilterModalComponent, { centered: true, size: 'lg' }); + const modalInstance = modalSpy.mock.results[0].value.componentInstance; + expect(modalInstance.filters).toEqual({ + tasks: ['task1'], + testCases: ['testCase1'], + occurrence: [component.minCount(), 5], + }); + expect(modalInstance.totalAmountOfTasks).toBe(component.totalAmountOfTasks); + expect(modalInstance.testCaseNames).toBe(component.testCaseNames); + expect(modalInstance.exerciseId).toBe(component.exerciseId); + expect(modalInstance.maxCount).toBe(component.maxCount); + }); + }); + + describe('applyFilters', () => { + it('should apply filters, update filter count, and reload data', () => { + const loadDataSpy = jest.spyOn(component, 'loadData' as any); + const countAppliedFiltersSpy = jest.spyOn(component, 'countAppliedFilters').mockReturnValue(2); + + const filters = { + tasks: ['task1'], + testCases: ['testCase1'], + occurrence: [component.minCount(), 10], + }; + + component.applyFilters(filters); + expect(countAppliedFiltersSpy).toHaveBeenCalledWith(filters); + expect(component.selectedFiltersCount()).toBe(2); + expect(loadDataSpy).toHaveBeenCalledOnce(); + }); + }); + + describe('countAppliedFilters', () => { + it('should count the applied filters correctly', () => { + component.maxCount.set(10); + const filters = { + tasks: ['task1', 'task2'], + testCases: ['testCase1'], + occurrence: [component.minCount(), component.maxCount()], + }; + const count = component.countAppliedFilters(filters); + + expect(count).toBe(3); + }); + + it('should return 0 if no filters are applied', () => { + const filters = { + tasks: [], + testCases: [], + occurrence: [component.minCount(), component.maxCount()], + }; + const count = component.countAppliedFilters(filters); + expect(count).toBe(0); + }); + }); }); diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts index 893d3e598b2f..5233bfbd52bf 100644 --- a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/feedback-analysis.service.spec.ts @@ -2,19 +2,26 @@ import { TestBed } from '@angular/core/testing'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { FeedbackAnalysisService, FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; import { provideHttpClient } from '@angular/common/http'; +import { SortingOrder } from 'app/shared/table/pageable-table'; describe('FeedbackAnalysisService', () => { let service: FeedbackAnalysisService; let httpMock: HttpTestingController; const feedbackDetailsMock: FeedbackDetail[] = [ - { detailText: 'Feedback 1', testCaseName: 'test1', count: 5, relativeCount: 25.0, taskNumber: 1 }, - { detailText: 'Feedback 2', testCaseName: 'test2', count: 3, relativeCount: 15.0, taskNumber: 2 }, + { detailText: 'Feedback 1', testCaseName: 'test1', count: 5, relativeCount: 25.0, taskNumber: '1', errorCategory: 'StudentError' }, + { detailText: 'Feedback 2', testCaseName: 'test2', count: 3, relativeCount: 15.0, taskNumber: '2', errorCategory: 'StudentError' }, ]; + const feedbackAnalysisResponseMock = { + feedbackDetails: { resultsOnPage: feedbackDetailsMock, numberOfPages: 1 }, + totalItems: 2, + totalAmountOfTasks: 2, + testCaseNames: ['test1', 'test2'], + }; + beforeEach(() => { TestBed.configureTestingModule({ - imports: [], providers: [provideHttpClient(), provideHttpClientTesting(), FeedbackAnalysisService], }); @@ -26,16 +33,39 @@ describe('FeedbackAnalysisService', () => { httpMock.verify(); }); - describe('getFeedbackDetailsForExercise', () => { + describe('search', () => { it('should retrieve feedback details for a given exercise', async () => { - const responsePromise = service.getFeedbackDetailsForExercise(1); + const pageable = { + page: 1, + pageSize: 10, + searchTerm: '', + sortingOrder: SortingOrder.ASCENDING, + sortedColumn: 'detailText', + }; + const filters = { tasks: [], testCases: [], occurrence: [] }; + const responsePromise = service.search(pageable, { exerciseId: 1, filters }); + + const req = httpMock.expectOne( + 'api/exercises/1/feedback-details?page=1&pageSize=10&searchTerm=&sortingOrder=ASCENDING&sortedColumn=detailText&filterTasks=&filterTestCases=&filterOccurrence=', + ); + expect(req.request.method).toBe('GET'); + req.flush(feedbackAnalysisResponseMock); + + const result = await responsePromise; + expect(result).toEqual(feedbackAnalysisResponseMock); + }); + }); + + describe('getMaxCount', () => { + it('should retrieve the max count for an exercise', async () => { + const responsePromise = service.getMaxCount(1); - const req = httpMock.expectOne('api/exercises/1/feedback-details'); + const req = httpMock.expectOne('api/exercises/1/feedback-details-max-count'); expect(req.request.method).toBe('GET'); - req.flush(feedbackDetailsMock); + req.flush(10); const result = await responsePromise; - expect(result).toEqual(feedbackDetailsMock); + expect(result).toBe(10); }); }); }); diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-filter-modal.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-filter-modal.component.spec.ts new file mode 100644 index 000000000000..369f7c42231e --- /dev/null +++ b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-filter-modal.component.spec.ts @@ -0,0 +1,97 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FeedbackFilterModalComponent } from 'app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-filter-modal.component'; +import { LocalStorageService } from 'ngx-webstorage'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { RangeSliderComponent } from 'app/shared/range-slider/range-slider.component'; + +describe('FeedbackFilterModalComponent', () => { + let fixture: ComponentFixture; + let component: FeedbackFilterModalComponent; + let localStorageService: LocalStorageService; + let activeModal: NgbActiveModal; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), ArtemisSharedCommonModule, RangeSliderComponent, FeedbackFilterModalComponent], + providers: [{ provide: LocalStorageService, useValue: { store: jest.fn(), clear: jest.fn(), retrieve: jest.fn() } }, NgbActiveModal], + }).compileComponents(); + + fixture = TestBed.createComponent(FeedbackFilterModalComponent); + component = fixture.componentInstance; + localStorageService = TestBed.inject(LocalStorageService); + activeModal = TestBed.inject(NgbActiveModal); + component.minCount.set(0); + component.maxCount.set(10); + fixture.detectChanges(); + }); + + it('should initialize filters correctly', () => { + component.filters = { + tasks: [], + testCases: [], + occurrence: [component.minCount(), component.maxCount()], + }; + + expect(component.filters).toEqual({ + tasks: [], + testCases: [], + occurrence: [0, 10], + }); + }); + + it('should call localStorage store when applying filters', () => { + const storeSpy = jest.spyOn(localStorageService, 'store'); + const emitSpy = jest.spyOn(component.filterApplied, 'emit'); + const closeSpy = jest.spyOn(activeModal, 'close'); + + component.filters.occurrence = [component.minCount(), component.maxCount()]; + component.applyFilter(); + + expect(storeSpy).toHaveBeenCalledWith(component.FILTER_TASKS_KEY, []); + expect(storeSpy).toHaveBeenCalledWith(component.FILTER_TEST_CASES_KEY, []); + expect(storeSpy).toHaveBeenCalledWith(component.FILTER_OCCURRENCE_KEY, [0, 10]); + expect(emitSpy).toHaveBeenCalledOnce(); + expect(closeSpy).toHaveBeenCalledOnce(); + }); + + it('should clear filters and reset them correctly', () => { + const clearSpy = jest.spyOn(localStorageService, 'clear'); + const emitSpy = jest.spyOn(component.filterApplied, 'emit'); + const closeSpy = jest.spyOn(activeModal, 'close'); + + component.clearFilter(); + + expect(clearSpy).toHaveBeenCalledWith(component.FILTER_TASKS_KEY); + expect(clearSpy).toHaveBeenCalledWith(component.FILTER_TEST_CASES_KEY); + expect(clearSpy).toHaveBeenCalledWith(component.FILTER_OCCURRENCE_KEY); + + expect(component.filters).toEqual({ + tasks: [], + testCases: [], + occurrence: [0, 10], + }); + expect(emitSpy).toHaveBeenCalledOnce(); + expect(closeSpy).toHaveBeenCalledOnce(); + }); + + it('should update filters when checkboxes change', () => { + const event = { target: { checked: true, value: 'test-task' } } as unknown as Event; + component.onCheckboxChange(event, 'tasks'); + expect(component.filters.tasks).toEqual(['test-task']); + }); + + it('should remove the value from filters when checkbox is unchecked', () => { + component.filters.tasks = ['test-task', 'task-2']; + const event = { target: { checked: false, value: 'test-task' } } as unknown as Event; + component.onCheckboxChange(event, 'tasks'); + expect(component.filters.tasks).toEqual(['task-2']); + }); + + it('should dismiss modal when closeModal is called', () => { + const dismissSpy = jest.spyOn(activeModal, 'dismiss'); + component.closeModal(); + expect(dismissSpy).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-modal.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-modal.component.spec.ts new file mode 100644 index 000000000000..66f0d3d33aeb --- /dev/null +++ b/src/test/javascript/spec/component/programming-exercise/feedback-analysis/modals/feedback-modal.component.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FeedbackModalComponent } from 'app/exercises/programming/manage/grading/feedback-analysis/Modal/feedback-modal.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { FeedbackDetail } from 'app/exercises/programming/manage/grading/feedback-analysis/feedback-analysis.service'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('FeedbackModalComponent', () => { + let fixture: ComponentFixture; + let component: FeedbackModalComponent; + let activeModal: NgbActiveModal; + + const mockFeedbackDetail: FeedbackDetail = { + count: 5, + relativeCount: 25.0, + detailText: 'Some feedback detail', + testCaseName: 'testCase1', + taskNumber: '1', + errorCategory: 'StudentError', + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), FeedbackModalComponent], + providers: [NgbActiveModal], + }).compileComponents(); + fixture = TestBed.createComponent(FeedbackModalComponent); + component = fixture.componentInstance; + activeModal = TestBed.inject(NgbActiveModal); + fixture.componentRef.setInput('feedbackDetail', mockFeedbackDetail); + fixture.detectChanges(); + }); + + it('should initialize with the provided feedback detail', () => { + expect(component.feedbackDetail()).toEqual(mockFeedbackDetail); + expect(component.feedbackDetail().detailText).toBe('Some feedback detail'); + expect(component.feedbackDetail().testCaseName).toBe('testCase1'); + }); + + it('should call close on activeModal when close is triggered', () => { + const closeSpy = jest.spyOn(activeModal, 'close'); + component.activeModal.close(); + expect(closeSpy).toHaveBeenCalledOnce(); + }); +}); From 83d37848a98d335ba172b7079c09ffaac201814d Mon Sep 17 00:00:00 2001 From: Tim Cremer <65229601+cremertim@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:22:10 +0200 Subject: [PATCH 10/18] Communication: Allow tutors to propose FAQ (#9477) --- .../repository/FaqRepository.java | 3 + .../communication/web/FaqResource.java | 66 ++++++++++++------- .../course-management-tab-bar.component.html | 2 +- .../course/manage/course-management.route.ts | 6 +- .../course-management-card.component.html | 2 +- src/main/webapp/app/entities/faq.model.ts | 6 +- .../webapp/app/faq/faq-update.component.ts | 28 ++++++-- src/main/webapp/app/faq/faq.component.html | 63 ++++++++++++------ src/main/webapp/app/faq/faq.component.ts | 58 +++++++++++++--- src/main/webapp/app/faq/faq.service.ts | 9 ++- .../course-faq/course-faq.component.ts | 6 +- src/main/webapp/i18n/de/faq.json | 16 +++-- src/main/webapp/i18n/en/faq.json | 14 +++- .../communication/FaqIntegrationTest.java | 55 ++++++++++++---- .../faq/faq-update.component.spec.ts | 60 +++++++++++++++-- .../spec/component/faq/faq.component.spec.ts | 44 +++++++++++-- .../course-faq/course-faq.component.spec.ts | 8 +-- .../spec/service/faq.service.spec.ts | 20 ++++++ 18 files changed, 365 insertions(+), 101 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java index bd8bb8989995..0361014a2076 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/repository/FaqRepository.java @@ -12,6 +12,7 @@ import org.springframework.transaction.annotation.Transactional; import de.tum.cit.aet.artemis.communication.domain.Faq; +import de.tum.cit.aet.artemis.communication.domain.FaqState; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; /** @@ -30,6 +31,8 @@ public interface FaqRepository extends ArtemisJpaRepository { """) Set findAllCategoriesByCourseId(@Param("courseId") Long courseId); + Set findAllByCourseIdAndFaqState(Long courseId, FaqState faqState); + @Transactional @Modifying void deleteAllByCourseId(Long courseId); diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java index 91a542aaa220..4f67dbb77ef6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/FaqResource.java @@ -23,14 +23,17 @@ import org.springframework.web.bind.annotation.RestController; import de.tum.cit.aet.artemis.communication.domain.Faq; +import de.tum.cit.aet.artemis.communication.domain.FaqState; import de.tum.cit.aet.artemis.communication.dto.FaqDTO; import de.tum.cit.aet.artemis.communication.repository.FaqRepository; import de.tum.cit.aet.artemis.core.domain.Course; +import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.repository.CourseRepository; import de.tum.cit.aet.artemis.core.security.Role; -import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; -import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastInstructorInCourse; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastStudentInCourse; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastTutorInCourse; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.core.util.HeaderUtil; @@ -56,10 +59,9 @@ public class FaqResource { private final FaqRepository faqRepository; public FaqResource(CourseRepository courseRepository, AuthorizationCheckService authCheckService, FaqRepository faqRepository) { - + this.faqRepository = faqRepository; this.courseRepository = courseRepository; this.authCheckService = authCheckService; - this.faqRepository = faqRepository; } /** @@ -72,18 +74,16 @@ public FaqResource(CourseRepository courseRepository, AuthorizationCheckService * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("courses/{courseId}/faqs") - @EnforceAtLeastInstructor + @EnforceAtLeastTutorInCourse public ResponseEntity createFaq(@RequestBody Faq faq, @PathVariable Long courseId) throws URISyntaxException { log.debug("REST request to save Faq : {}", faq); if (faq.getId() != null) { throw new BadRequestAlertException("A new faq cannot already have an ID", ENTITY_NAME, "idExists"); } - + checkPriviledgeForAcceptedElseThrow(faq, courseId); if (faq.getCourse() == null || !faq.getCourse().getId().equals(courseId)) { throw new BadRequestAlertException("Course ID in path and FAQ do not match", ENTITY_NAME, "courseIdMismatch"); } - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); - Faq savedFaq = faqRepository.save(faq); FaqDTO dto = new FaqDTO(savedFaq); return ResponseEntity.created(new URI("/api/courses/" + courseId + "/faqs/" + savedFaq.getId())).body(dto); @@ -99,14 +99,15 @@ public ResponseEntity createFaq(@RequestBody Faq faq, @PathVariable Long * if the faq is not valid or if the faq course id does not match with the path variable */ @PutMapping("courses/{courseId}/faqs/{faqId}") - @EnforceAtLeastInstructor + @EnforceAtLeastTutorInCourse public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable Long faqId, @PathVariable Long courseId) { log.debug("REST request to update Faq : {}", faq); if (faqId == null || !faqId.equals(faq.getId())) { throw new BadRequestAlertException("Id of FAQ and path must match", ENTITY_NAME, "idNull"); } - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, faq.getCourse(), null); + checkPriviledgeForAcceptedElseThrow(faq, courseId); Faq existingFaq = faqRepository.findByIdElseThrow(faqId); + checkPriviledgeForAcceptedElseThrow(existingFaq, courseId); if (!Objects.equals(existingFaq.getCourse().getId(), courseId)) { throw new BadRequestAlertException("Course ID of the FAQ provided courseID must match", ENTITY_NAME, "idNull"); } @@ -115,6 +116,19 @@ public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable Long return ResponseEntity.ok().body(dto); } + /** + * @param faq the faq to be checked * + * @param courseId the id of the course the faq belongs to + * @throws AccessForbiddenException if the user is not an instructor + * + */ + private void checkPriviledgeForAcceptedElseThrow(Faq faq, Long courseId) { + if (faq.getFaqState() == FaqState.ACCEPTED) { + Course course = courseRepository.findByIdElseThrow(courseId); + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, null); + } + } + /** * GET /courses/:courseId/faqs/:faqId : get the faq with the id faqId. * @@ -123,14 +137,13 @@ public ResponseEntity updateFaq(@RequestBody Faq faq, @PathVariable Long * @return the ResponseEntity with status 200 (OK) and with body the faq, or with status 404 (Not Found) */ @GetMapping("courses/{courseId}/faqs/{faqId}") - @EnforceAtLeastStudent + @EnforceAtLeastStudentInCourse public ResponseEntity getFaq(@PathVariable Long faqId, @PathVariable Long courseId) { log.debug("REST request to get faq {}", faqId); Faq faq = faqRepository.findByIdElseThrow(faqId); if (faq.getCourse() == null || !faq.getCourse().getId().equals(courseId)) { throw new BadRequestAlertException("Course ID in path and FAQ do not match", ENTITY_NAME, "courseIdMismatch"); } - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, faq.getCourse(), null); FaqDTO dto = new FaqDTO(faq); return ResponseEntity.ok(dto); } @@ -143,12 +156,11 @@ public ResponseEntity getFaq(@PathVariable Long faqId, @PathVariable Lon * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("courses/{courseId}/faqs/{faqId}") - @EnforceAtLeastInstructor + @EnforceAtLeastInstructorInCourse public ResponseEntity deleteFaq(@PathVariable Long faqId, @PathVariable Long courseId) { log.debug("REST request to delete faq {}", faqId); Faq existingFaq = faqRepository.findByIdElseThrow(faqId); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, existingFaq.getCourse(), null); if (!Objects.equals(existingFaq.getCourse().getId(), courseId)) { throw new BadRequestAlertException("Course ID of the FAQ provided courseID must match", ENTITY_NAME, "idNull"); } @@ -163,17 +175,30 @@ public ResponseEntity deleteFaq(@PathVariable Long faqId, @PathVariable Lo * @return the ResponseEntity with status 200 (OK) and the list of faqs in body */ @GetMapping("courses/{courseId}/faqs") - @EnforceAtLeastStudent + @EnforceAtLeastStudentInCourse public ResponseEntity> getFaqForCourse(@PathVariable Long courseId) { log.debug("REST request to get all Faqs for the course with id : {}", courseId); - - Course course = courseRepository.findByIdElseThrow(courseId); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); Set faqs = faqRepository.findAllByCourseId(courseId); Set faqDTOS = faqs.stream().map(FaqDTO::new).collect(Collectors.toSet()); return ResponseEntity.ok().body(faqDTOS); } + /** + * GET /courses/:courseId/faq-status/:faqState : get all the faqs of a course in the specified status + * + * @param courseId the courseId of the course for which all faqs should be returned + * @param faqState the state of all returned FAQs + * @return the ResponseEntity with status 200 (OK) and the list of faqs in body + */ + @GetMapping("courses/{courseId}/faq-state/{faqState}") + @EnforceAtLeastStudentInCourse + public ResponseEntity> getAllFaqsForCourseByStatus(@PathVariable Long courseId, @PathVariable FaqState faqState) { + log.debug("REST request to get all Faqs for the course with id : " + courseId + "and status " + faqState, courseId); + Set faqs = faqRepository.findAllByCourseIdAndFaqState(courseId, faqState); + Set faqDTOS = faqs.stream().map(FaqDTO::new).collect(Collectors.toSet()); + return ResponseEntity.ok().body(faqDTOS); + } + /** * GET /courses/:courseId/faq-categories : get all the faq categories of a course * @@ -181,12 +206,9 @@ public ResponseEntity> getFaqForCourse(@PathVariable Long courseId) * @return the ResponseEntity with status 200 (OK) and the list of faqs in body */ @GetMapping("courses/{courseId}/faq-categories") - @EnforceAtLeastStudent + @EnforceAtLeastStudentInCourse public ResponseEntity> getFaqCategoriesForCourse(@PathVariable Long courseId) { log.debug("REST request to get all Faq Categories for the course with id : {}", courseId); - - Course course = courseRepository.findByIdElseThrow(courseId); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, null); Set faqs = faqRepository.findAllCategoriesByCourseId(courseId); return ResponseEntity.ok().body(faqs); diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html index bc10fd753a7c..3b98b59baf42 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html @@ -72,7 +72,7 @@ } - @if (course.isAtLeastInstructor && course.faqEnabled) { + @if (course.isAtLeastTutor && course.faqEnabled) { diff --git a/src/main/webapp/app/course/manage/course-management.route.ts b/src/main/webapp/app/course/manage/course-management.route.ts index c85789f6d74e..9d00cb7cc48f 100644 --- a/src/main/webapp/app/course/manage/course-management.route.ts +++ b/src/main/webapp/app/course/manage/course-management.route.ts @@ -347,7 +347,7 @@ export const courseManagementState: Routes = [ course: CourseManagementResolve, }, data: { - authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], + authorities: [Authority.TA, Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], pageTitle: 'artemisApp.faq.home.title', }, canActivate: [UserRouteAccessService], @@ -363,7 +363,7 @@ export const courseManagementState: Routes = [ path: 'new', component: FaqUpdateComponent, data: { - authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], + authorities: [Authority.TA, Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], pageTitle: 'global.generic.create', }, canActivate: [UserRouteAccessService], @@ -378,7 +378,7 @@ export const courseManagementState: Routes = [ path: 'edit', component: FaqUpdateComponent, data: { - authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], + authorities: [Authority.TA, Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], pageTitle: 'global.generic.edit', }, canActivate: [UserRouteAccessService], diff --git a/src/main/webapp/app/course/manage/overview/course-management-card.component.html b/src/main/webapp/app/course/manage/overview/course-management-card.component.html index d13dee53b6e5..2e19cd151508 100644 --- a/src/main/webapp/app/course/manage/overview/course-management-card.component.html +++ b/src/main/webapp/app/course/manage/overview/course-management-card.component.html @@ -339,7 +339,7 @@

    }

    @@ -67,6 +67,10 @@

    + + + + @@ -86,6 +90,9 @@

    + +

    +
    @for (category of faq.categories; track category) { @@ -93,27 +100,41 @@

    }

    -
    + @if (faq.faqState == FaqState.PROPOSED && isAtLeastInstructor) { +
    + + +
    + }
    - - - - - - + @if (isAtLeastInstructor || faq.faqState !== FaqState.ACCEPTED) { + + + + + } + @if (isAtLeastInstructor) { + + }
    diff --git a/src/main/webapp/app/faq/faq.component.ts b/src/main/webapp/app/faq/faq.component.ts index 0a8c1df808f8..2dbdef5f3aca 100644 --- a/src/main/webapp/app/faq/faq.component.ts +++ b/src/main/webapp/app/faq/faq.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit, inject } from '@angular/core'; -import { Faq } from 'app/entities/faq.model'; -import { faEdit, faFilter, faPencilAlt, faPlus, faSort, faTrash } from '@fortawesome/free-solid-svg-icons'; -import { BehaviorSubject, Subject } from 'rxjs'; +import { Faq, FaqState } from 'app/entities/faq.model'; +import { faCancel, faCheck, faEdit, faFilter, faPencilAlt, faPlus, faSort, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import { debounceTime, map } from 'rxjs/operators'; import { AlertService } from 'app/core/util/alert.service'; import { ActivatedRoute } from '@angular/router'; @@ -16,6 +16,9 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; +import { AccountService } from 'app/core/auth/account.service'; +import { Course } from 'app/entities/course.model'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'jhi-faq', @@ -25,14 +28,18 @@ import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.co imports: [ArtemisSharedModule, CustomExerciseCategoryBadgeComponent, ArtemisSharedComponentModule, ArtemisMarkdownModule, SearchFilterComponent], }) export class FaqComponent implements OnInit, OnDestroy { + protected readonly FaqState = FaqState; faqs: Faq[]; + course: Course; filteredFaqs: Faq[]; existingCategories: FaqCategory[]; courseId: number; hasCategories: boolean = false; + isAtLeastInstructor = false; private dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); + private routeDataSubscription: Subscription; activeFilters = new Set(); searchInput = new BehaviorSubject(''); @@ -40,17 +47,21 @@ export class FaqComponent implements OnInit, OnDestroy { ascending: boolean; // Icons - faEdit = faEdit; - faPlus = faPlus; - faTrash = faTrash; - faPencilAlt = faPencilAlt; - faFilter = faFilter; - faSort = faSort; + protected readonly faEdit = faEdit; + protected readonly faPlus = faPlus; + protected readonly faTrash = faTrash; + protected readonly faPencilAlt = faPencilAlt; + protected readonly faFilter = faFilter; + protected readonly faSort = faSort; + protected readonly faCancel = faCancel; + protected readonly faCheck = faCheck; private faqService = inject(FaqService); private route = inject(ActivatedRoute); private alertService = inject(AlertService); private sortService = inject(SortService); + private accountService = inject(AccountService); + private translateService = inject(TranslateService); constructor() { this.predicate = 'id'; @@ -64,11 +75,19 @@ export class FaqComponent implements OnInit, OnDestroy { this.searchInput.pipe(debounceTime(300)).subscribe((searchTerm: string) => { this.refreshFaqList(searchTerm); }); + this.routeDataSubscription = this.route.data.subscribe((data) => { + const course = data['course']; + if (course) { + this.course = course; + this.isAtLeastInstructor = this.accountService.isAtLeastInstructorInCourse(course); + } + }); } ngOnDestroy(): void { this.dialogErrorSource.complete(); this.searchInput.complete(); + this.routeDataSubscription?.unsubscribe(); } deleteFaq(courseId: number, faqId: number) { @@ -142,4 +161,25 @@ export class FaqComponent implements OnInit, OnDestroy { this.applyFilters(); this.applySearch(searchTerm); } + + updateFaqState(courseId: number, faq: Faq, newState: FaqState, successMessageKey: string) { + const previousState = faq.faqState; + faq.faqState = newState; + faq.course = this.course; + this.faqService.update(courseId, faq).subscribe({ + next: () => this.alertService.success(successMessageKey, { title: faq.questionTitle }), + error: (error: HttpErrorResponse) => { + this.dialogErrorSource.next(error.message); + faq.faqState = previousState; + }, + }); + } + + rejectFaq(courseId: number, faq: Faq) { + this.updateFaqState(courseId, faq, FaqState.REJECTED, 'artemisApp.faq.rejected'); + } + + acceptProposedFaq(courseId: number, faq: Faq) { + this.updateFaqState(courseId, faq, FaqState.ACCEPTED, 'artemisApp.faq.accepted'); + } } diff --git a/src/main/webapp/app/faq/faq.service.ts b/src/main/webapp/app/faq/faq.service.ts index bcdd824c671c..ed6edc06c57e 100644 --- a/src/main/webapp/app/faq/faq.service.ts +++ b/src/main/webapp/app/faq/faq.service.ts @@ -16,7 +16,6 @@ export class FaqService { create(courseId: number, faq: Faq): Observable { const copy = FaqService.convertFaqFromClient(faq); - copy.faqState = FaqState.ACCEPTED; return this.http.post(`${this.resourceUrl}/${courseId}/faqs`, copy, { observe: 'response' }).pipe( map((res: EntityResponseType) => { return res; @@ -47,6 +46,14 @@ export class FaqService { .pipe(map((res: EntityArrayResponseType) => FaqService.convertFaqCategoryArrayFromServer(res))); } + findAllByCourseIdAndState(courseId: number, faqState: FaqState): Observable { + return this.http + .get(`${this.resourceUrl}/${courseId}/faq-state/${faqState}`, { + observe: 'response', + }) + .pipe(map((res: EntityArrayResponseType) => FaqService.convertFaqCategoryArrayFromServer(res))); + } + delete(courseId: number, faqId: number): Observable> { return this.http.delete(`${this.resourceUrl}/${courseId}/faqs/${faqId}`, { observe: 'response' }); } diff --git a/src/main/webapp/app/overview/course-faq/course-faq.component.ts b/src/main/webapp/app/overview/course-faq/course-faq.component.ts index db5a91e2c3d7..571e2a2dee1a 100644 --- a/src/main/webapp/app/overview/course-faq/course-faq.component.ts +++ b/src/main/webapp/app/overview/course-faq/course-faq.component.ts @@ -7,7 +7,7 @@ import { ButtonType } from 'app/shared/components/button.component'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { CourseFaqAccordionComponent } from 'app/overview/course-faq/course-faq-accordion-component'; -import { Faq } from 'app/entities/faq.model'; +import { Faq, FaqState } from 'app/entities/faq.model'; import { FaqService } from 'app/faq/faq.service'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; @@ -45,7 +45,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { readonly ButtonType = ButtonType; // Icons - faFilter = faFilter; + readonly faFilter = faFilter; private route = inject(ActivatedRoute); @@ -72,7 +72,7 @@ export class CourseFaqComponent implements OnInit, OnDestroy { private loadFaqs() { this.faqService - .findAllByCourseId(this.courseId) + .findAllByCourseIdAndState(this.courseId, FaqState.ACCEPTED) .pipe(map((res: HttpResponse) => res.body)) .subscribe({ next: (res: Faq[]) => { diff --git a/src/main/webapp/i18n/de/faq.json b/src/main/webapp/i18n/de/faq.json index 987b093de2c7..76440a96998b 100644 --- a/src/main/webapp/i18n/de/faq.json +++ b/src/main/webapp/i18n/de/faq.json @@ -4,12 +4,19 @@ "home": { "title": "FAQ", "createLabel": "FAQ erstellen", + "proposeLabel": "FAQ vorschlagen", + "accept": "FAQ akzeptieren", + "reject": "FAQ ablehnen", "filterLabel": "Filter", "createOrEditLabel": "FAQ erstellen oder bearbeiten" }, - "created": "Das FAQ wurde erfolgreich erstellt", - "updated": "Das FAQ wurde erfolgreich aktualisiert", - "deleted": "Das FAQ wurde erfolgreich gelöscht", + "created": "Die FAQ {{ title }} wurde erfolgreich erstellt", + "updated": "Die FAQ {{ title }} wurde erfolgreich aktualisiert", + "proposed": "Die FAQ {{ title }} wurde erfolgreich vorgeschlagen", + "proposedChange": "Die Änderungen am FAQ {{ title }} wurden erfolgreich vorgeschlagen", + "deleted": "Die FAQ wurde erfolgreich gelöscht", + "accepted": "Die FAQ {{ title }} wurde erfolgreich akzeptiert", + "rejected": "Die FAQ {{ title }} wurde erfolgreich abgelehnt", "delete": { "question": "Soll die FAQ {{ title }} wirklich dauerhaft gelöscht werden? Diese Aktion kann NICHT rückgängig gemacht werden!", "typeNameToConfirm": "Bitte gib den Namen des FAQ zur Bestätigung ein." @@ -18,7 +25,8 @@ "table": { "questionTitle": "Fragentitel", "questionAnswer": "Antwort auf die Frage", - "categories": "Kategorien" + "categories": "Kategorien", + "state": "Status" }, "course": "Kurs", "noExisting": "Momentan existiert für diesen Kurs noch kein FAQ.", diff --git a/src/main/webapp/i18n/en/faq.json b/src/main/webapp/i18n/en/faq.json index 3fd403409d2b..98f9fc345060 100644 --- a/src/main/webapp/i18n/en/faq.json +++ b/src/main/webapp/i18n/en/faq.json @@ -4,12 +4,19 @@ "home": { "title": "FAQ", "createLabel": "Create a new FAQ", + "proposeLabel": "Propose a new FAQ", + "accept": "Accept FAQ", + "reject": "Reject FAQ", "filterLabel": "Filter", "createOrEditLabel": "Create or edit FAQ" }, - "created": "The FAQ was successfully created", - "updated": "The FAQ was successfully updated", + "created": "The FAQ {{ title }} was successfully created", + "updated": "The FAQ {{ title }} was successfully updated", + "proposed": "The FAQ {{ title }} was successfully proposed", + "proposedChange": "The changes to the FAQ {{ title }} have been successfully proposed", "deleted": "The FAQ was successfully deleted", + "accepted": "The FAQ {{ title }} was successfully accepted", + "rejected": "The FAQ {{ title }} was successfully rejected", "delete": { "question": "Are you sure you want to permanently delete the FAQ {{ title }}? This action can NOT be undone!", "typeNameToConfirm": "Please type in the name of the FAQ to confirm." @@ -18,7 +25,8 @@ "table": { "questionTitle": "Question title", "questionAnswer": "Question answer", - "categories": "Categories" + "categories": "Categories", + "state": "State" }, "course": "Course", "noExisting": "Currently, there is no FAQ available for this course.", diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/FaqIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/FaqIntegrationTest.java index 5f226f52d4dc..b655873250f6 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/FaqIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/FaqIntegrationTest.java @@ -15,6 +15,7 @@ import de.tum.cit.aet.artemis.communication.domain.Faq; import de.tum.cit.aet.artemis.communication.domain.FaqState; +import de.tum.cit.aet.artemis.communication.dto.FaqDTO; import de.tum.cit.aet.artemis.communication.repository.FaqRepository; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; @@ -29,6 +30,8 @@ class FaqIntegrationTest extends AbstractSpringIntegrationIndependentTest { private Course course1; + private Course course2; + private Faq faq; @BeforeEach @@ -37,7 +40,8 @@ void initTestCase() throws Exception { userUtilService.addUsers(TEST_PREFIX, 1, numberOfTutors, 0, 1); List courses = courseUtilService.createCoursesWithExercisesAndLectures(TEST_PREFIX, true, true, numberOfTutors); this.course1 = this.courseRepository.findByIdWithExercisesAndExerciseDetailsAndLecturesElseThrow(courses.getFirst().getId()); - this.faq = FaqFactory.generateFaq(course1, FaqState.ACCEPTED, "answer", "title"); + this.course2 = courses.getLast(); + this.faq = FaqFactory.generateFaq(course1, FaqState.PROPOSED, "answer", "title"); faqRepository.save(this.faq); // Add users that are not in the course userUtilService.createAndSaveUser(TEST_PREFIX + "student42"); @@ -51,12 +55,6 @@ private void testAllPreAuthorize() throws Exception { request.delete("/api/courses/" + faq.getCourse().getId() + "/faqs/" + this.faq.getId(), HttpStatus.FORBIDDEN); } - @Test - @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") - void testAll_asTutor() throws Exception { - this.testAllPreAuthorize(); - } - @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testAll_asStudent() throws Exception { @@ -88,7 +86,7 @@ void createFaq_alreadyId_shouldReturnBadRequest() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void createFaq_courseId_noMatch_shouldReturnBadRequest() throws Exception { Faq newFaq = FaqFactory.generateFaq(course1, FaqState.ACCEPTED, "title", "answer"); - request.postWithResponseBody("/api/courses/" + course1.getId() + 1 + "/faqs", newFaq, Faq.class, HttpStatus.BAD_REQUEST); + request.postWithResponseBody("/api/courses/" + course2.getId() + "/faqs", newFaq, Faq.class, HttpStatus.BAD_REQUEST); } @Test @@ -116,7 +114,26 @@ void updateFaq_IdsDoNotMatch_shouldNotUpdateFaq() throws Exception { Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(); faq.setQuestionTitle("Updated"); faq.setFaqState(FaqState.PROPOSED); - Faq updatedFaq = request.putWithResponseBody("/api/courses/" + faq.getCourse().getId() + 1 + "/faqs/" + faq.getId(), faq, Faq.class, HttpStatus.BAD_REQUEST); + faq.setId(faq.getId() + 1); + Faq updatedFaq = request.putWithResponseBody("/api/courses/" + course1.getId() + "/faqs/" + (faq.getId() - 1), faq, Faq.class, HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") + void updateFaq_Tutor_cannotAcceptFaq() throws Exception { + Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(); + faq.setQuestionTitle("Updated"); + faq.setFaqState(FaqState.ACCEPTED); + Faq updatedFaq = request.putWithResponseBody("/api/courses/" + faq.getCourse().getId() + "/faqs/" + faq.getId(), faq, Faq.class, HttpStatus.FORBIDDEN); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void updateFaq_Instructor_canAcceptFaq() throws Exception { + Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(); + faq.setQuestionTitle("Updated"); + faq.setFaqState(FaqState.ACCEPTED); + Faq updatedFaq = request.putWithResponseBody("/api/courses/" + faq.getCourse().getId() + "/faqs/" + faq.getId(), faq, Faq.class, HttpStatus.OK); } @Test @@ -140,7 +157,7 @@ void testGetFaqByFaqId() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testGetFaqByFaqId_shouldNotGet_IdMismatch() throws Exception { Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(EntityNotFoundException::new); - Faq returnedFaq = request.get("/api/courses/" + faq.getCourse().getId() + 1 + "/faqs/" + faq.getId(), HttpStatus.BAD_REQUEST, Faq.class); + Faq returnedFaq = request.get("/api/courses/" + course2.getId() + "/faqs/" + faq.getId(), HttpStatus.BAD_REQUEST, Faq.class); } @Test @@ -156,9 +173,25 @@ void deleteFaq_shouldDeleteFAQ() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void deleteFaq_IdsDoNotMatch_shouldNotDeleteFAQ() throws Exception { Faq faq = faqRepository.findById(this.faq.getId()).orElseThrow(EntityNotFoundException::new); - request.delete("/api/courses/" + faq.getCourse().getId() + 1 + "/faqs/" + faq.getId(), HttpStatus.BAD_REQUEST); + request.delete("/api/courses/" + course2.getId() + "/faqs/" + faq.getId(), HttpStatus.BAD_REQUEST); Optional faqOptional = faqRepository.findById(faq.getId()); assertThat(faqOptional).isPresent(); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void getFaq_shouldGetFaqByCourseId() throws Exception { + Set faqs = faqRepository.findAllByCourseIdAndFaqState(this.course1.getId(), FaqState.PROPOSED); + Set returnedFaqs = request.get("/api/courses/" + course1.getId() + "/faqs", HttpStatus.OK, Set.class); + assertThat(returnedFaqs).hasSize(faqs.size()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void getFaq_shouldGetFaqByCourseIdAndState() throws Exception { + Set faqs = faqRepository.findAllByCourseIdAndFaqState(this.course1.getId(), FaqState.PROPOSED); + Set returnedFaqs = request.get("/api/courses/" + course1.getId() + "/faq-state/" + "PROPOSED", HttpStatus.OK, Set.class); + assertThat(returnedFaqs).hasSize(faqs.size()); + } + } diff --git a/src/test/javascript/spec/component/faq/faq-update.component.spec.ts b/src/test/javascript/spec/component/faq/faq-update.component.spec.ts index 04c04b3d12d3..275bd4cb4f48 100644 --- a/src/test/javascript/spec/component/faq/faq-update.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq-update.component.spec.ts @@ -11,7 +11,7 @@ import { MockTranslateService } from '../../helpers/mocks/service/mock-translate import { ArtemisTestModule } from '../../test.module'; import { FaqUpdateComponent } from 'app/faq/faq-update.component'; import { FaqService } from 'app/faq/faq.service'; -import { Faq, FaqState } from 'app/entities/faq.model'; +import { Faq } from 'app/entities/faq.model'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AlertService } from 'app/core/util/alert.service'; import { FaqCategory } from 'app/entities/faq-category.model'; @@ -93,6 +93,7 @@ describe('FaqUpdateComponent', () => { it('should create faq', fakeAsync(() => { faqUpdateComponent.faq = { questionTitle: 'test1' } as Faq; + faqUpdateComponent.isAtLeastInstructor = true; const createSpy = jest.spyOn(faqService, 'create').mockReturnValue( of( new HttpResponse({ @@ -111,13 +112,38 @@ describe('FaqUpdateComponent', () => { faqUpdateComponent.save(); tick(); - expect(createSpy).toHaveBeenCalledExactlyOnceWith(courseId, { faqState: FaqState.ACCEPTED, questionTitle: 'test1' }); + expect(createSpy).toHaveBeenCalledExactlyOnceWith(courseId, { questionTitle: 'test1', faqState: 'ACCEPTED' }); + expect(faqUpdateComponent.isSaving).toBeFalse(); + })); + + it('should propose faq', fakeAsync(() => { + faqUpdateComponent.faq = { questionTitle: 'test1' } as Faq; + faqUpdateComponent.isAtLeastInstructor = false; + const createSpy = jest.spyOn(faqService, 'create').mockReturnValue( + of( + new HttpResponse({ + body: { + id: 3, + questionTitle: 'test1', + course: { + id: 1, + }, + } as Faq, + }), + ), + ); + + faqUpdateComponentFixture.detectChanges(); + faqUpdateComponent.save(); + tick(); + + expect(createSpy).toHaveBeenCalledExactlyOnceWith(courseId, { questionTitle: 'test1', faqState: 'PROPOSED' }); expect(faqUpdateComponent.isSaving).toBeFalse(); })); it('should edit a faq', fakeAsync(() => { activatedRoute.parent!.data = of({ course: { id: 1 }, faq: { id: 6 } }); - + faqUpdateComponent.isAtLeastInstructor = true; faqUpdateComponentFixture.detectChanges(); faqUpdateComponent.faq = { id: 6, questionTitle: 'test1Updated' } as Faq; @@ -139,8 +165,34 @@ describe('FaqUpdateComponent', () => { faqUpdateComponent.save(); tick(); faqUpdateComponentFixture.detectChanges(); + expect(updateSpy).toHaveBeenCalledExactlyOnceWith(courseId, { id: 6, questionTitle: 'test1Updated', faqState: 'ACCEPTED' }); + })); + + it('should propose to edit a faq', fakeAsync(() => { + activatedRoute.parent!.data = of({ course: { id: 1 }, faq: { id: 6 } }); + faqUpdateComponent.isAtLeastInstructor = false; + faqUpdateComponentFixture.detectChanges(); + faqUpdateComponent.faq = { id: 6, questionTitle: 'test1Updated' } as Faq; - expect(updateSpy).toHaveBeenCalledExactlyOnceWith(courseId, { id: 6, questionTitle: 'test1Updated' }); + const updateSpy = jest.spyOn(faqService, 'update').mockReturnValue( + of>( + new HttpResponse({ + body: { + id: 6, + questionTitle: 'test1Updated', + questionAnswer: 'answer', + course: { + id: 1, + }, + } as Faq, + }), + ), + ); + + faqUpdateComponent.save(); + tick(); + faqUpdateComponentFixture.detectChanges(); + expect(updateSpy).toHaveBeenCalledExactlyOnceWith(courseId, { id: 6, questionTitle: 'test1Updated', faqState: 'PROPOSED' }); })); it('should navigate to previous state', fakeAsync(() => { diff --git a/src/test/javascript/spec/component/faq/faq.component.spec.ts b/src/test/javascript/spec/component/faq/faq.component.spec.ts index d9f8b84c6cd5..6fe12d58bb4c 100644 --- a/src/test/javascript/spec/component/faq/faq.component.spec.ts +++ b/src/test/javascript/spec/component/faq/faq.component.spec.ts @@ -9,7 +9,7 @@ import { MockRouter } from '../../helpers/mocks/mock-router'; import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; import { ArtemisTestModule } from '../../test.module'; import { FaqService } from 'app/faq/faq.service'; -import { Faq } from 'app/entities/faq.model'; +import { Faq, FaqState } from 'app/entities/faq.model'; import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown-editor.module'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -18,6 +18,8 @@ import { FaqCategory } from 'app/entities/faq-category.model'; import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; import { AlertService } from 'app/core/util/alert.service'; import { SortService } from 'app/shared/service/sort.service'; +import { MockAccountService } from '../../helpers/mocks/service/mock-account.service'; +import { AccountService } from 'app/core/auth/account.service'; function createFaq(id: number, category: string, color: string): Faq { const faq = new Faq(); @@ -25,6 +27,7 @@ function createFaq(id: number, category: string, color: string): Faq { faq.questionTitle = 'questionTitle'; faq.questionAnswer = 'questionAnswer'; faq.categories = [new FaqCategory(category, color)]; + faq.faqState = FaqState.PROPOSED; return faq; } @@ -57,12 +60,11 @@ describe('FaqComponent', () => { providers: [ { provide: TranslateService, useClass: MockTranslateService }, { provide: Router, useClass: MockRouter }, + { provide: AccountService, useClass: MockAccountService }, { provide: ActivatedRoute, useValue: { - parent: { - data: of({ course: { id: 1 } }), - }, + data: of({ course: { id: 1 } }), snapshot: { paramMap: convertToParamMap({ courseId: '1', @@ -188,4 +190,38 @@ describe('FaqComponent', () => { faqComponent.sortRows(); expect(sortService.sortByProperty).toHaveBeenCalledOnce(); }); + + it('should reject faq properly', () => { + jest.spyOn(faqService, 'update').mockReturnValue(of(new HttpResponse({ body: faq1 }))); + faqComponentFixture.detectChanges(); + faqComponent.rejectFaq(courseId, faq1); + expect(faqService.update).toHaveBeenCalledExactlyOnceWith(courseId, faq1); + expect(faq1.faqState).toEqual(FaqState.REJECTED); + }); + + it('should not change status if rejection fails', () => { + const error = { status: 500 }; + jest.spyOn(faqService, 'update').mockReturnValue(throwError(() => new HttpErrorResponse(error))); + faqComponentFixture.detectChanges(); + faqComponent.rejectFaq(courseId, faq1); + expect(faqService.update).toHaveBeenCalledExactlyOnceWith(courseId, faq1); + expect(faq1.faqState).toEqual(FaqState.PROPOSED); + }); + + it('should accepts proposed faq properly', () => { + jest.spyOn(faqService, 'update').mockReturnValue(of(new HttpResponse({ body: faq1 }))); + faqComponentFixture.detectChanges(); + faqComponent.acceptProposedFaq(courseId, faq1); + expect(faqService.update).toHaveBeenCalledExactlyOnceWith(courseId, faq1); + expect(faq1.faqState).toEqual(FaqState.ACCEPTED); + }); + + it('should not change status if acceptance fails', () => { + const error = { status: 500 }; + jest.spyOn(faqService, 'update').mockReturnValue(throwError(() => new HttpErrorResponse(error))); + faqComponentFixture.detectChanges(); + faqComponent.acceptProposedFaq(courseId, faq1); + expect(faqService.update).toHaveBeenCalledExactlyOnceWith(courseId, faq1); + expect(faq1.faqState).toEqual(FaqState.PROPOSED); + }); }); diff --git a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts index f7795a433603..43a2e61cbf5a 100644 --- a/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-faq/course-faq.component.spec.ts @@ -16,7 +16,7 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo import { ArtemisSharedModule } from 'app/shared/shared.module'; import { CustomExerciseCategoryBadgeComponent } from 'app/shared/exercise-categories/custom-exercise-category-badge/custom-exercise-category-badge.component'; import { CourseFaqAccordionComponent } from 'app/overview/course-faq/course-faq-accordion-component'; -import { Faq } from 'app/entities/faq.model'; +import { Faq, FaqState } from 'app/entities/faq.model'; import { FaqCategory } from 'app/entities/faq-category.model'; import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; @@ -69,7 +69,7 @@ describe('CourseFaqs', () => { }, }, MockProvider(FaqService, { - findAllByCourseId: () => { + findAllByCourseIdAndState: () => { return of( new HttpResponse({ body: [faq1, faq2, faq3], @@ -118,10 +118,10 @@ describe('CourseFaqs', () => { }); it('should fetch faqs when initialized', () => { - const findAllSpy = jest.spyOn(faqService, 'findAllByCourseId'); + const findAllSpy = jest.spyOn(faqService, 'findAllByCourseIdAndState'); courseFaqComponentFixture.detectChanges(); - expect(findAllSpy).toHaveBeenCalledExactlyOnceWith(1); + expect(findAllSpy).toHaveBeenCalledExactlyOnceWith(1, FaqState.ACCEPTED); expect(courseFaqComponent.faqs).toHaveLength(3); }); diff --git a/src/test/javascript/spec/service/faq.service.spec.ts b/src/test/javascript/spec/service/faq.service.spec.ts index 272a7352a091..ab4960d98354 100644 --- a/src/test/javascript/spec/service/faq.service.spec.ts +++ b/src/test/javascript/spec/service/faq.service.spec.ts @@ -134,6 +134,26 @@ describe('Faq Service', () => { expect(expectedResult.body).toEqual(expected); }); + it('should find faqs by courseId and status', () => { + const category = { + color: '#6ae8ac', + category: 'category1', + } as FaqCategory; + const returnedFromService = [{ ...elemDefault, categories: [JSON.stringify(category)] }]; + const expected = [{ ...elemDefault, categories: [new FaqCategory('category1', '#6ae8ac')] }]; + const courseId = 1; + service + .findAllByCourseIdAndState(courseId, FaqState.ACCEPTED) + .pipe(take(1)) + .subscribe((resp) => (expectedResult = resp)); + const req = httpMock.expectOne({ + url: `api/courses/${courseId}/faq-state/${FaqState.ACCEPTED}`, + method: 'GET', + }); + req.flush(returnedFromService); + expect(expectedResult.body).toEqual(expected); + }); + it('should find all categories by courseId', () => { const category = { color: '#6ae8ac', From a0d723f6dcee7b5ed90dfa285b05938a03d0a826 Mon Sep 17 00:00:00 2001 From: Mohamed Bilel Besrour <58034472+BBesrour@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:36:30 +0200 Subject: [PATCH 11/18] Integrated code lifecycle: Add build agent name (#9529) --- docs/admin/setup/distributed.rst | 5 +++ .../artemis/buildagent/dto/BuildAgentDTO.java | 10 +++++ .../buildagent/dto/BuildAgentInformation.java | 4 +- .../buildagent/dto/BuildJobQueueItem.java | 17 ++++--- .../service/SharedQueueProcessingService.java | 42 ++++++++++++----- .../web/admin/AdminBuildJobQueueResource.java | 10 +++-- .../programming/domain/build/BuildJob.java | 2 +- .../localci/LocalCIQueueWebsocketService.java | 8 ++-- .../LocalCIResultProcessingService.java | 8 ++-- .../localci/LocalCITriggerService.java | 7 ++- .../LocalCIWebsocketMessagingService.java | 2 +- .../localci/SharedQueueManagementService.java | 4 +- .../config/application-buildagent.yml | 1 + src/main/resources/config/application.yml | 9 ++++ .../build-agent-information.model.ts | 19 ++++++++ .../entities/programming/build-agent.model.ts | 19 ++------ .../entities/programming/build-job.model.ts | 3 +- .../build-agent-details.component.html | 11 +++-- .../build-agent-details.component.ts | 18 ++++---- .../build-agent-summary.component.html | 20 ++++++--- .../build-agent-summary.component.ts | 19 +++++--- .../build-agents/build-agents.service.ts | 10 ++--- .../build-queue/build-queue.component.html | 10 ++--- src/main/webapp/i18n/de/buildAgents.json | 1 + src/main/webapp/i18n/en/buildAgents.json | 1 + .../service/BuildAgentDockerServiceTest.java | 5 ++- .../icl/LocalCIIntegrationTest.java | 6 +-- .../icl/LocalCIResourceIntegrationTest.java | 45 ++++++++++++------- .../programming/icl/LocalCIServiceTest.java | 8 +++- .../build-agent-details.component.spec.ts | 24 +++++----- .../build-agent-summary.component.spec.ts | 20 ++++----- .../build-agents/build-agents.service.spec.ts | 12 ++--- src/test/resources/config/application.yml | 2 + 33 files changed, 242 insertions(+), 140 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentDTO.java create mode 100644 src/main/webapp/app/entities/programming/build-agent-information.model.ts diff --git a/docs/admin/setup/distributed.rst b/docs/admin/setup/distributed.rst index 1fa74024dc2d..b2d1a12822d3 100644 --- a/docs/admin/setup/distributed.rst +++ b/docs/admin/setup/distributed.rst @@ -617,8 +617,13 @@ These credentials are used to clone repositories via HTTPS. You must also add th container-cleanup: expiry-minutes: 5 # Time after a hanging container will automatically be removed cleanup-schedule-minutes: 60 # Schedule for container cleanup + build-agent: + short-name: "artemis-build-agent-X" # Short name of the build agent. This should be unique for each build agent. Only lowercase letters, numbers and hyphens are allowed. + display-name: "Artemis Build Agent X" # This value is optional. If omitted, the short name will be used as display name. Display name of the build agent. This is shown in the Artemis UI. +Please note that ``artemis.continuous-integration.build-agent.short-name`` must be provided. Otherwise, the build agent will not start. + Build agents run as `Hazelcast Lite Members `__ and require a full member, in our case a core node, to be running. Thus, before starting a build agent make sure that at least the primary node is running. You can then add and remove build agents to the cluster as desired. diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentDTO.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentDTO.java new file mode 100644 index 000000000000..2ffa0f2daa61 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentDTO.java @@ -0,0 +1,10 @@ +package de.tum.cit.aet.artemis.buildagent.dto; + +import java.io.Serial; +import java.io.Serializable; + +public record BuildAgentDTO(String name, String memberAddress, String displayName) implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentInformation.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentInformation.java index ab24012f51fa..40af5049060e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentInformation.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildAgentInformation.java @@ -11,7 +11,7 @@ // in the future are migrated or cleared. Changes should be communicated in release notes as potentially breaking changes. @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record BuildAgentInformation(String name, int maxNumberOfConcurrentBuildJobs, int numberOfCurrentBuildJobs, List runningBuildJobs, +public record BuildAgentInformation(BuildAgentDTO buildAgent, int maxNumberOfConcurrentBuildJobs, int numberOfCurrentBuildJobs, List runningBuildJobs, BuildAgentStatus status, List recentBuildJobs, String publicSshKey) implements Serializable { @Serial @@ -24,7 +24,7 @@ public record BuildAgentInformation(String name, int maxNumberOfConcurrentBuildJ * @param recentBuildJobs The list of recent build jobs */ public BuildAgentInformation(BuildAgentInformation agentInformation, List recentBuildJobs) { - this(agentInformation.name(), agentInformation.maxNumberOfConcurrentBuildJobs(), agentInformation.numberOfCurrentBuildJobs(), agentInformation.runningBuildJobs, + this(agentInformation.buildAgent(), agentInformation.maxNumberOfConcurrentBuildJobs(), agentInformation.numberOfCurrentBuildJobs(), agentInformation.runningBuildJobs, agentInformation.status(), recentBuildJobs, agentInformation.publicSshKey()); } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobQueueItem.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobQueueItem.java index d9bfff039c2e..7a4220399819 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobQueueItem.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobQueueItem.java @@ -14,7 +14,7 @@ // in the future are migrated or cleared. Changes should be communicated in release notes as potentially breaking changes. @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record BuildJobQueueItem(String id, String name, String buildAgentAddress, long participationId, long courseId, long exerciseId, int retryCount, int priority, +public record BuildJobQueueItem(String id, String name, BuildAgentDTO buildAgent, long participationId, long courseId, long exerciseId, int retryCount, int priority, BuildStatus status, RepositoryInfo repositoryInfo, JobTimingInfo jobTimingInfo, BuildConfig buildConfig, ResultDTO submissionResult) implements Serializable { @Serial @@ -28,7 +28,7 @@ public record BuildJobQueueItem(String id, String name, String buildAgentAddress * @param status The status/result of the build job */ public BuildJobQueueItem(BuildJobQueueItem queueItem, ZonedDateTime buildCompletionDate, BuildStatus status) { - this(queueItem.id(), queueItem.name(), queueItem.buildAgentAddress(), queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), + this(queueItem.id(), queueItem.name(), queueItem.buildAgent(), queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), queueItem.priority(), status, queueItem.repositoryInfo(), new JobTimingInfo(queueItem.jobTimingInfo.submissionDate(), queueItem.jobTimingInfo.buildStartDate(), buildCompletionDate), queueItem.buildConfig(), null); } @@ -36,17 +36,16 @@ public BuildJobQueueItem(BuildJobQueueItem queueItem, ZonedDateTime buildComplet /** * Constructor used to create a new processing build job from a queued build job * - * @param queueItem The queued build job - * @param hazelcastMemberAddress The address of the hazelcast member that is processing the build job + * @param queueItem The queued build job + * @param buildAgent The build agent that will process the build job */ - public BuildJobQueueItem(BuildJobQueueItem queueItem, String hazelcastMemberAddress) { - this(queueItem.id(), queueItem.name(), hazelcastMemberAddress, queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), - queueItem.priority(), null, queueItem.repositoryInfo(), new JobTimingInfo(queueItem.jobTimingInfo.submissionDate(), ZonedDateTime.now(), null), - queueItem.buildConfig(), null); + public BuildJobQueueItem(BuildJobQueueItem queueItem, BuildAgentDTO buildAgent) { + this(queueItem.id(), queueItem.name(), buildAgent, queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), queueItem.priority(), + null, queueItem.repositoryInfo(), new JobTimingInfo(queueItem.jobTimingInfo.submissionDate(), ZonedDateTime.now(), null), queueItem.buildConfig(), null); } public BuildJobQueueItem(BuildJobQueueItem queueItem, ResultDTO submissionResult) { - this(queueItem.id(), queueItem.name(), queueItem.buildAgentAddress(), queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), + this(queueItem.id(), queueItem.name(), queueItem.buildAgent(), queueItem.participationId(), queueItem.courseId(), queueItem.exerciseId(), queueItem.retryCount(), queueItem.priority(), queueItem.status(), queueItem.repositoryInfo(), queueItem.jobTimingInfo(), queueItem.buildConfig(), submissionResult); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java index e73f8bac244b..0823ec5a4f9b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/SharedQueueProcessingService.java @@ -25,6 +25,7 @@ import jakarta.annotation.PreDestroy; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; @@ -45,6 +46,7 @@ import com.hazelcast.map.IMap; import com.hazelcast.topic.ITopic; +import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentInformation; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; import de.tum.cit.aet.artemis.buildagent.dto.BuildResult; @@ -116,9 +118,15 @@ public class SharedQueueProcessingService { */ private final AtomicBoolean processResults = new AtomicBoolean(true); - @Value("${artemis.continuous-integration.pause-grace-period-seconds:15}") + @Value("${artemis.continuous-integration.pause-grace-period-seconds:60}") private int pauseGracePeriodSeconds; + @Value("${artemis.continuous-integration.build-agent.short-name}") + private String buildAgentShortName; + + @Value("${artemis.continuous-integration.build-agent.display-name:}") + private String buildAgentDisplayName; + public SharedQueueProcessingService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, ExecutorService localCIBuildExecutorService, BuildJobManagementService buildJobManagementService, BuildLogsMap buildLogsMap, BuildAgentSshKeyService buildAgentSSHKeyService, TaskScheduler taskScheduler) { this.hazelcastInstance = hazelcastInstance; @@ -134,6 +142,17 @@ public SharedQueueProcessingService(@Qualifier("hazelcastInstance") HazelcastIns */ @EventListener(ApplicationReadyEvent.class) public void init() { + if (!buildAgentShortName.matches("^[a-z0-9-]+$")) { + String errorMessage = "Build agent short name must not be empty and only contain lowercase letters, numbers and hyphens." + + " Build agent short name should be changed in the application properties under 'artemis.continuous-integration.build-agent.short-name'."; + log.error(errorMessage); + throw new IllegalArgumentException(errorMessage); + } + + if (StringUtils.isBlank(buildAgentDisplayName)) { + buildAgentDisplayName = buildAgentShortName; + } + this.buildAgentInformation = this.hazelcastInstance.getMap("buildAgentInformation"); this.processingJobs = this.hazelcastInstance.getMap("processingJobs"); this.queue = this.hazelcastInstance.getQueue("buildJobQueue"); @@ -154,14 +173,14 @@ public void init() { ITopic pauseBuildAgentTopic = hazelcastInstance.getTopic("pauseBuildAgentTopic"); pauseBuildAgentTopic.addMessageListener(message -> { - if (message.getMessageObject().equals(hazelcastInstance.getCluster().getLocalMember().getAddress().toString())) { + if (buildAgentShortName.equals(message.getMessageObject())) { pauseBuildAgent(); } }); ITopic resumeBuildAgentTopic = hazelcastInstance.getTopic("resumeBuildAgentTopic"); resumeBuildAgentTopic.addMessageListener(message -> { - if (message.getMessageObject().equals(hazelcastInstance.getCluster().getLocalMember().getAddress().toString())) { + if (buildAgentShortName.equals(message.getMessageObject())) { resumeBuildAgent(); } }); @@ -201,6 +220,7 @@ public void updateBuildAgentInformation() { log.debug("There are only lite member in the cluster. Not updating build agent information."); return; } + // Remove build agent information of offline nodes removeOfflineNodes(); @@ -253,7 +273,7 @@ private void checkAvailabilityAndProcessNextBuild() { if (buildJob != null) { processingJobs.remove(buildJob.id()); - buildJob = new BuildJobQueueItem(buildJob, ""); + buildJob = new BuildJobQueueItem(buildJob, new BuildAgentDTO("", "", "")); log.info("Adding build job back to the queue: {}", buildJob); queue.add(buildJob); localProcessingJobs.decrementAndGet(); @@ -275,7 +295,7 @@ private BuildJobQueueItem addToProcessingJobs() { if (buildJob != null) { String hazelcastMemberAddress = hazelcastInstance.getCluster().getLocalMember().getAddress().toString(); - BuildJobQueueItem processingJob = new BuildJobQueueItem(buildJob, hazelcastMemberAddress); + BuildJobQueueItem processingJob = new BuildJobQueueItem(buildJob, new BuildAgentDTO(buildAgentShortName, hazelcastMemberAddress, buildAgentDisplayName)); processingJobs.put(processingJob.id(), processingJob); localProcessingJobs.incrementAndGet(); @@ -297,10 +317,10 @@ private void updateLocalBuildAgentInformationWithRecentJob(BuildJobQueueItem rec // Add/update BuildAgentInformation info = getUpdatedLocalBuildAgentInformation(recentBuildJob); try { - buildAgentInformation.put(info.name(), info); + buildAgentInformation.put(info.buildAgent().memberAddress(), info); } catch (Exception e) { - log.error("Error while updating build agent information for agent {}", info.name(), e); + log.error("Error while updating build agent information for agent {} with address {}", info.buildAgent().name(), info.buildAgent().memberAddress(), e); } } finally { @@ -334,11 +354,13 @@ private BuildAgentInformation getUpdatedLocalBuildAgentInformation(BuildJobQueue String publicSshKey = buildAgentSSHKeyService.getPublicKeyAsString(); - return new BuildAgentInformation(memberAddress, maxNumberOfConcurrentBuilds, numberOfCurrentBuildJobs, processingJobsOfMember, status, recentBuildJobs, publicSshKey); + BuildAgentDTO agentInfo = new BuildAgentDTO(buildAgentShortName, memberAddress, buildAgentDisplayName); + + return new BuildAgentInformation(agentInfo, maxNumberOfConcurrentBuilds, numberOfCurrentBuildJobs, processingJobsOfMember, status, recentBuildJobs, publicSshKey); } private List getProcessingJobsOfNode(String memberAddress) { - return processingJobs.values().stream().filter(job -> Objects.equals(job.buildAgentAddress(), memberAddress)).toList(); + return processingJobs.values().stream().filter(job -> Objects.equals(job.buildAgent().memberAddress(), memberAddress)).toList(); } private void removeOfflineNodes() { @@ -371,7 +393,7 @@ private void processBuild(BuildJobQueueItem buildJob) { log.debug("Build job completed: {}", buildJob); JobTimingInfo jobTimingInfo = new JobTimingInfo(buildJob.jobTimingInfo().submissionDate(), buildJob.jobTimingInfo().buildStartDate(), ZonedDateTime.now()); - BuildJobQueueItem finishedJob = new BuildJobQueueItem(buildJob.id(), buildJob.name(), buildJob.buildAgentAddress(), buildJob.participationId(), buildJob.courseId(), + BuildJobQueueItem finishedJob = new BuildJobQueueItem(buildJob.id(), buildJob.name(), buildJob.buildAgent(), buildJob.participationId(), buildJob.courseId(), buildJob.exerciseId(), buildJob.retryCount(), buildJob.priority(), BuildStatus.SUCCESSFUL, buildJob.repositoryInfo(), jobTimingInfo, buildJob.buildConfig(), null); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java index 33e7bd099155..27cd50a6e4b9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java @@ -4,6 +4,7 @@ import java.time.ZonedDateTime; import java.util.List; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -95,9 +96,12 @@ public ResponseEntity> getBuildAgentSummary() { @GetMapping("build-agent") public ResponseEntity getBuildAgentDetails(@RequestParam String agentName) { log.debug("REST request to get information on build agent {}", agentName); - BuildAgentInformation buildAgentDetails = localCIBuildJobQueueService.getBuildAgentInformation().stream().filter(agent -> agent.name().equals(agentName)).findFirst() - .orElse(null); - return ResponseEntity.ok(buildAgentDetails); + Optional buildAgentDetails = localCIBuildJobQueueService.getBuildAgentInformation().stream() + .filter(agent -> agent.buildAgent().name().equals(agentName)).findFirst(); + if (buildAgentDetails.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(buildAgentDetails.get()); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/build/BuildJob.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/build/BuildJob.java index 53f3b75305f2..7a6aeafdbd04 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/build/BuildJob.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/build/BuildJob.java @@ -88,7 +88,7 @@ public BuildJob(BuildJobQueueItem queueItem, BuildStatus buildStatus, Result res this.courseId = queueItem.courseId(); this.participationId = queueItem.participationId(); this.result = result; - this.buildAgentAddress = queueItem.buildAgentAddress(); + this.buildAgentAddress = queueItem.buildAgent().memberAddress(); this.buildStartDate = queueItem.jobTimingInfo().buildStartDate(); this.buildCompletionDate = queueItem.jobTimingInfo().buildCompletionDate(); this.repositoryType = queueItem.repositoryInfo().repositoryType(); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java index bfd04f5ba49d..5a805ff54d03 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIQueueWebsocketService.java @@ -84,7 +84,7 @@ private void sendBuildAgentSummaryOverWebsocket() { } private void sendBuildAgentDetailsOverWebsocket(String agentName) { - sharedQueueManagementService.getBuildAgentInformation().stream().filter(agent -> agent.name().equals(agentName)).findFirst() + sharedQueueManagementService.getBuildAgentInformation().stream().filter(agent -> agent.buildAgent().name().equals(agentName)).findFirst() .ifPresent(localCIWebsocketMessagingService::sendBuildAgentDetails); } @@ -127,19 +127,19 @@ private class BuildAgentListener @Override public void entryAdded(com.hazelcast.core.EntryEvent event) { log.debug("Build agent added: {}", event.getValue()); - sendBuildAgentInformationOverWebsocket(event.getValue().name()); + sendBuildAgentInformationOverWebsocket(event.getValue().buildAgent().name()); } @Override public void entryRemoved(com.hazelcast.core.EntryEvent event) { log.debug("Build agent removed: {}", event.getOldValue()); - sendBuildAgentInformationOverWebsocket(event.getOldValue().name()); + sendBuildAgentInformationOverWebsocket(event.getOldValue().buildAgent().name()); } @Override public void entryUpdated(com.hazelcast.core.EntryEvent event) { log.debug("Build agent updated: {}", event.getValue()); - sendBuildAgentInformationOverWebsocket(event.getValue().name()); + sendBuildAgentInformationOverWebsocket(event.getValue().buildAgent().name()); } } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java index a450fc545886..71edf64a3fa8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIResultProcessingService.java @@ -225,8 +225,8 @@ public void processResult() { */ private void addResultToBuildAgentsRecentBuildJobs(BuildJobQueueItem buildJob, Result result) { try { - buildAgentInformation.lock(buildJob.buildAgentAddress()); - BuildAgentInformation buildAgent = buildAgentInformation.get(buildJob.buildAgentAddress()); + buildAgentInformation.lock(buildJob.buildAgent().memberAddress()); + BuildAgentInformation buildAgent = buildAgentInformation.get(buildJob.buildAgent().memberAddress()); if (buildAgent != null) { List recentBuildJobs = buildAgent.recentBuildJobs(); for (int i = 0; i < recentBuildJobs.size(); i++) { @@ -235,11 +235,11 @@ private void addResultToBuildAgentsRecentBuildJobs(BuildJobQueueItem buildJob, R break; } } - buildAgentInformation.put(buildJob.buildAgentAddress(), new BuildAgentInformation(buildAgent, recentBuildJobs)); + buildAgentInformation.put(buildJob.buildAgent().memberAddress(), new BuildAgentInformation(buildAgent, recentBuildJobs)); } } finally { - buildAgentInformation.unlock(buildJob.buildAgentAddress()); + buildAgentInformation.unlock(buildJob.buildAgent().memberAddress()); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java index 5da2860ba799..cb4e894c90f7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCITriggerService.java @@ -22,6 +22,7 @@ import com.hazelcast.core.HazelcastInstance; import com.hazelcast.map.IMap; +import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildConfig; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; import de.tum.cit.aet.artemis.buildagent.dto.JobTimingInfo; @@ -196,8 +197,10 @@ else if (triggeredByPushTo.equals(RepositoryType.TESTS)) { 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); + BuildAgentDTO buildAgent = new BuildAgentDTO(null, null, null); + + BuildJobQueueItem buildJobQueueItem = new BuildJobQueueItem(buildJobId, participation.getBuildPlanId(), buildAgent, participation.getId(), courseId, + programmingExercise.getId(), 0, priority, null, repositoryInfo, jobTimingInfo, buildConfig, null); queue.add(buildJobQueueItem); log.info("Added build job {} to the queue", buildJobId); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIWebsocketMessagingService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIWebsocketMessagingService.java index 7c527a155b49..e27ec440d5aa 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIWebsocketMessagingService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIWebsocketMessagingService.java @@ -100,7 +100,7 @@ public void sendBuildAgentSummary(List buildAgentInfo) { } public void sendBuildAgentDetails(BuildAgentInformation buildAgentDetails) { - String channel = "/topic/admin/build-agent/" + buildAgentDetails.name(); + String channel = "/topic/admin/build-agent/" + buildAgentDetails.buildAgent().name(); log.debug("Sending message on topic {}: {}", channel, buildAgentDetails); websocketMessagingService.sendMessage(channel, buildAgentDetails); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java index 9af5fe3b45c1..87b44d4872ba 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/SharedQueueManagementService.java @@ -137,7 +137,7 @@ public List getBuildAgentInformation() { } public List getBuildAgentInformationWithoutRecentBuildJobs() { - return buildAgentInformation.values().stream().map(agent -> new BuildAgentInformation(agent.name(), agent.maxNumberOfConcurrentBuildJobs(), + return buildAgentInformation.values().stream().map(agent -> new BuildAgentInformation(agent.buildAgent(), agent.maxNumberOfConcurrentBuildJobs(), agent.numberOfCurrentBuildJobs(), agent.runningBuildJobs(), agent.status(), null, null)).toList(); } @@ -208,7 +208,7 @@ public void cancelAllRunningBuildJobs() { * @param agentName name of the agent */ public void cancelAllRunningBuildJobsForAgent(String agentName) { - processingJobs.values().stream().filter(job -> Objects.equals(job.buildAgentAddress(), agentName)).forEach(job -> cancelBuildJob(job.id())); + processingJobs.values().stream().filter(job -> Objects.equals(job.buildAgent().name(), agentName)).forEach(job -> cancelBuildJob(job.id())); } /** diff --git a/src/main/resources/config/application-buildagent.yml b/src/main/resources/config/application-buildagent.yml index 1439567b2cc9..fc3e4847f25e 100644 --- a/src/main/resources/config/application-buildagent.yml +++ b/src/main/resources/config/application-buildagent.yml @@ -33,6 +33,7 @@ artemis: container-cleanup: expiry-minutes: 5 cleanup-schedule-minutes: 60 + pause-grace-period-seconds: 60 git: name: Artemis email: artemis@xcit.tum.de diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 85965bb400e0..adcd08605bdb 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -98,6 +98,15 @@ artemis: typescript: default: "ghcr.io/ls1intum/artemis-javascript-docker:v1.0.0" + # The following properties are used to configure the Artemis build agent. + # The build agent is responsible for executing the buildJob to test student submissions. + build-agent: + # Name of the build agent. Only lowercase letters, numbers and hyphens are allowed. ([a-z0-9-]+) + short-name: "artemis-build-agent-1" + display-name: "Artemis Build Agent 1" + + + management: endpoints: web: diff --git a/src/main/webapp/app/entities/programming/build-agent-information.model.ts b/src/main/webapp/app/entities/programming/build-agent-information.model.ts new file mode 100644 index 000000000000..c9e5c3c9e71d --- /dev/null +++ b/src/main/webapp/app/entities/programming/build-agent-information.model.ts @@ -0,0 +1,19 @@ +import { BaseEntity } from 'app/shared/model/base-entity'; +import { BuildJob } from 'app/entities/programming/build-job.model'; +import { BuildAgent } from 'app/entities/programming/build-agent.model'; + +export enum BuildAgentStatus { + ACTIVE = 'ACTIVE', + PAUSED = 'PAUSED', + IDLE = 'IDLE', +} + +export class BuildAgentInformation implements BaseEntity { + public id?: number; + public buildAgent?: BuildAgent; + public maxNumberOfConcurrentBuildJobs?: number; + public numberOfCurrentBuildJobs?: number; + public runningBuildJobs?: BuildJob[]; + public status?: BuildAgentStatus; + public recentBuildJobs?: BuildJob[]; +} diff --git a/src/main/webapp/app/entities/programming/build-agent.model.ts b/src/main/webapp/app/entities/programming/build-agent.model.ts index 86c51ffc8b35..f67bfbcd9dd1 100644 --- a/src/main/webapp/app/entities/programming/build-agent.model.ts +++ b/src/main/webapp/app/entities/programming/build-agent.model.ts @@ -1,18 +1,5 @@ -import { BaseEntity } from 'app/shared/model/base-entity'; -import { BuildJob } from 'app/entities/programming/build-job.model'; - -export enum BuildAgentStatus { - ACTIVE = 'ACTIVE', - PAUSED = 'PAUSED', - IDLE = 'IDLE', -} - -export class BuildAgent implements BaseEntity { - public id?: number; +export class BuildAgent { public name?: string; - public maxNumberOfConcurrentBuildJobs?: number; - public numberOfCurrentBuildJobs?: number; - public runningBuildJobs?: BuildJob[]; - public status?: BuildAgentStatus; - public recentBuildJobs?: BuildJob[]; + public memberAddress?: string; + public displayName?: string; } diff --git a/src/main/webapp/app/entities/programming/build-job.model.ts b/src/main/webapp/app/entities/programming/build-job.model.ts index a36bb9756091..2c88534d2626 100644 --- a/src/main/webapp/app/entities/programming/build-job.model.ts +++ b/src/main/webapp/app/entities/programming/build-job.model.ts @@ -4,11 +4,12 @@ import { JobTimingInfo } from 'app/entities/job-timing-info.model'; import { BuildConfig } from 'app/entities/programming/build-config.model'; import { Result } from 'app/entities/result.model'; import dayjs from 'dayjs/esm'; +import { BuildAgent } from 'app/entities/programming/build-agent.model'; export class BuildJob implements StringBaseEntity { public id?: string; public name?: string; - public buildAgentAddress?: string; + public buildAgent?: BuildAgent; public participationId?: number; public courseId?: number; public exerciseId?: number; diff --git a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html index 1a88dba6a6d8..ca64b85617f6 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html +++ b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.html @@ -2,10 +2,13 @@ @if (buildAgent) {
    -
    -

    - : -

    {{ buildAgent.name }}

    +
    +
    +

    + : +

    {{ buildAgent.buildAgent?.displayName }}

    +
    + {{ buildAgent.buildAgent?.name }} - {{ buildAgent.buildAgent?.memberAddress }}
    @if (buildAgent.status === 'PAUSED') { diff --git a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts index de19544b01ad..1408b83c2495 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts +++ b/src/main/webapp/app/localci/build-agents/build-agent-details/build-agent-details/build-agent-details.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { BuildAgent } from 'app/entities/programming/build-agent.model'; +import { BuildAgentInformation } from 'app/entities/programming/build-agent-information.model'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; import { Subscription } from 'rxjs'; import { faCircleCheck, faExclamationCircle, faExclamationTriangle, faPause, faPlay, faTimes } from '@fortawesome/free-solid-svg-icons'; @@ -17,7 +17,7 @@ import { AlertService, AlertType } from 'app/core/util/alert.service'; }) export class BuildAgentDetailsComponent implements OnInit, OnDestroy { protected readonly TriggeredByPushTo = TriggeredByPushTo; - buildAgent: BuildAgent; + buildAgent: BuildAgentInformation; agentName: string; websocketSubscription: Subscription; restSubscription: Subscription; @@ -77,7 +77,7 @@ export class BuildAgentDetailsComponent implements OnInit, OnDestroy { }); } - private updateBuildAgent(buildAgent: BuildAgent) { + private updateBuildAgent(buildAgent: BuildAgentInformation) { this.buildAgent = buildAgent; this.setRecentBuildJobsDuration(); } @@ -100,8 +100,8 @@ export class BuildAgentDetailsComponent implements OnInit, OnDestroy { } cancelAllBuildJobs() { - if (this.buildAgent.name) { - this.buildQueueService.cancelAllRunningBuildJobsForAgent(this.buildAgent.name).subscribe(); + if (this.buildAgent.buildAgent?.name) { + this.buildQueueService.cancelAllRunningBuildJobsForAgent(this.buildAgent.buildAgent?.name).subscribe(); } } @@ -111,8 +111,8 @@ export class BuildAgentDetailsComponent implements OnInit, OnDestroy { } pauseBuildAgent(): void { - if (this.buildAgent.name) { - this.buildAgentsService.pauseBuildAgent(this.buildAgent.name).subscribe({ + if (this.buildAgent.buildAgent?.name) { + this.buildAgentsService.pauseBuildAgent(this.buildAgent.buildAgent.name).subscribe({ next: () => { this.alertService.addAlert({ type: AlertType.SUCCESS, @@ -135,8 +135,8 @@ export class BuildAgentDetailsComponent implements OnInit, OnDestroy { } resumeBuildAgent(): void { - if (this.buildAgent.name) { - this.buildAgentsService.resumeBuildAgent(this.buildAgent.name).subscribe({ + if (this.buildAgent.buildAgent?.name) { + this.buildAgentsService.resumeBuildAgent(this.buildAgent.buildAgent.name).subscribe({ next: () => { this.alertService.addAlert({ type: AlertType.SUCCESS, diff --git a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html index 0982463d75a9..7021b3f79c4d 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html +++ b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.html @@ -20,19 +20,27 @@

    - + - + - + - - + + {{ value }} + + + + + + + + @@ -75,7 +83,7 @@

    {{ value }} @if (value > 0) { - diff --git a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts index b7f232bf0fc4..711fa4a40dce 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts +++ b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts @@ -1,11 +1,12 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { BuildAgent } from 'app/entities/programming/build-agent.model'; +import { BuildAgentInformation } from 'app/entities/programming/build-agent-information.model'; import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; import { Subscription } from 'rxjs'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { BuildQueueService } from 'app/localci/build-queue/build-queue.service'; import { Router } from '@angular/router'; +import { BuildAgent } from 'app/entities/programming/build-agent.model'; @Component({ selector: 'jhi-build-agents', @@ -13,7 +14,7 @@ import { Router } from '@angular/router'; styleUrl: './build-agent-summary.component.scss', }) export class BuildAgentSummaryComponent implements OnInit, OnDestroy { - buildAgents: BuildAgent[] = []; + buildAgents: BuildAgentInformation[] = []; buildCapacity = 0; currentBuilds = 0; channel: string = '/topic/admin/build-agents'; @@ -56,7 +57,7 @@ export class BuildAgentSummaryComponent implements OnInit, OnDestroy { }); } - private updateBuildAgents(buildAgents: BuildAgent[]) { + private updateBuildAgents(buildAgents: BuildAgentInformation[]) { this.buildAgents = buildAgents; this.buildCapacity = this.buildAgents.reduce((sum, agent) => sum + (agent.maxNumberOfConcurrentBuildJobs || 0), 0); this.currentBuilds = this.buildAgents.reduce((sum, agent) => sum + (agent.numberOfCurrentBuildJobs || 0), 0); @@ -75,10 +76,14 @@ export class BuildAgentSummaryComponent implements OnInit, OnDestroy { this.buildQueueService.cancelBuildJob(buildJobId).subscribe(); } - cancelAllBuildJobs(buildAgentName: string) { - const buildAgent = this.buildAgents.find((agent) => agent.name === buildAgentName); - if (buildAgent && buildAgent.name) { - this.buildQueueService.cancelAllRunningBuildJobsForAgent(buildAgent.name).subscribe(); + cancelAllBuildJobs(buildAgent?: BuildAgent) { + if (!buildAgent?.name) { + return; + } + + const buildAgentToCancel = this.buildAgents.find((agent) => agent.buildAgent?.name === buildAgent.name); + if (buildAgentToCancel?.buildAgent?.name) { + this.buildQueueService.cancelAllRunningBuildJobsForAgent(buildAgentToCancel.buildAgent?.name).subscribe(); } } } diff --git a/src/main/webapp/app/localci/build-agents/build-agents.service.ts b/src/main/webapp/app/localci/build-agents/build-agents.service.ts index b701b85fe5d0..6aac582940d3 100644 --- a/src/main/webapp/app/localci/build-agents/build-agents.service.ts +++ b/src/main/webapp/app/localci/build-agents/build-agents.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; -import { BuildAgent } from 'app/entities/programming/build-agent.model'; +import { BuildAgentInformation } from 'app/entities/programming/build-agent-information.model'; import { catchError } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) @@ -13,15 +13,15 @@ export class BuildAgentsService { /** * Get all build agents */ - getBuildAgentSummary(): Observable { - return this.http.get(`${this.adminResourceUrl}/build-agents`); + getBuildAgentSummary(): Observable { + return this.http.get(`${this.adminResourceUrl}/build-agents`); } /** * Get build agent details */ - getBuildAgentDetails(agentName: string): Observable { - return this.http.get(`${this.adminResourceUrl}/build-agent`, { params: { agentName } }).pipe( + getBuildAgentDetails(agentName: string): Observable { + return this.http.get(`${this.adminResourceUrl}/build-agent`, { params: { agentName } }).pipe( catchError((err) => { return throwError(() => new Error(`Failed to fetch build agent details ${agentName}\n${err.message}`)); }), diff --git a/src/main/webapp/app/localci/build-queue/build-queue.component.html b/src/main/webapp/app/localci/build-queue/build-queue.component.html index 4e0e1e095754..a8f9a582cbf3 100644 --- a/src/main/webapp/app/localci/build-queue/build-queue.component.html +++ b/src/main/webapp/app/localci/build-queue/build-queue.component.html @@ -107,15 +107,15 @@

    + - + - + - - + + {{ value }} diff --git a/src/main/webapp/i18n/de/buildAgents.json b/src/main/webapp/i18n/de/buildAgents.json index 2cd4ca72def3..186bd1432540 100644 --- a/src/main/webapp/i18n/de/buildAgents.json +++ b/src/main/webapp/i18n/de/buildAgents.json @@ -5,6 +5,7 @@ "summary": "Build Agenten Zusammenfassung", "details": "Build Agenten Details", "name": "Name", + "memberAddress": "Agentenadresse", "maxNumberOfConcurrentBuildJobs": "Maximale Anzahl an parallelen Build Jobs", "numberOfCurrentBuildJobs": "Anzahl aktueller Build Jobs", "runningBuildJobs": "Laufende Build Jobs", diff --git a/src/main/webapp/i18n/en/buildAgents.json b/src/main/webapp/i18n/en/buildAgents.json index e1401d97e272..1610e1bd0847 100644 --- a/src/main/webapp/i18n/en/buildAgents.json +++ b/src/main/webapp/i18n/en/buildAgents.json @@ -5,6 +5,7 @@ "summary": "Build Agents Summary", "details": "Build Agents Details", "name": "Name", + "memberAddress": "Member Address", "maxNumberOfConcurrentBuildJobs": "Max # of concurrent build jobs", "numberOfCurrentBuildJobs": "# of current build jobs", "runningBuildJobs": "Running build jobs", diff --git a/src/test/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerServiceTest.java index b4012d1f7a6f..eab5c2190d4c 100644 --- a/src/test/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/buildagent/service/BuildAgentDockerServiceTest.java @@ -28,6 +28,7 @@ import com.hazelcast.core.HazelcastInstance; import com.hazelcast.map.IMap; +import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildConfig; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; import de.tum.cit.aet.artemis.core.exception.LocalCIException; @@ -92,7 +93,9 @@ void testPullDockerImage() { doReturn(inspectImageCmd).when(dockerClient).inspectImageCmd(anyString()); doThrow(new NotFoundException("")).when(inspectImageCmd).exec(); BuildConfig buildConfig = new BuildConfig("echo 'test'", "test-image-name", "test", "test", "test", "test", null, null, false, false, false, null, 0, null, null, null); - var build = new BuildJobQueueItem("1", "job1", "address1", 1, 1, 1, 1, 1, BuildStatus.SUCCESSFUL, null, null, buildConfig, null); + + BuildAgentDTO buildAgent = new BuildAgentDTO("buildagent1", "address1", "buildagent1"); + var build = new BuildJobQueueItem("1", "job1", buildAgent, 1, 1, 1, 1, 1, BuildStatus.SUCCESSFUL, null, null, buildConfig, null); // Pull image try { buildAgentDockerService.pullDockerImage(build, new BuildLogsMap()); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java index 0be99f7c9aa1..bbaa18df71b1 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java @@ -486,8 +486,8 @@ void testCustomCheckoutPaths() { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testPauseAndResumeBuildAgent() { - String memberAddress = hazelcastInstance.getCluster().getLocalMember().getAddress().toString(); - hazelcastInstance.getTopic("pauseBuildAgentTopic").publish(memberAddress); + String buildAgentName = "artemis-build-agent-test"; + hazelcastInstance.getTopic("pauseBuildAgentTopic").publish(buildAgentName); ProgrammingExerciseStudentParticipation studentParticipation = localVCLocalCITestService.createParticipation(programmingExercise, student1Login); @@ -500,7 +500,7 @@ void testPauseAndResumeBuildAgent() { return buildJobQueueItem != null && buildJobQueueItem.buildConfig().commitHashToBuild().equals(commitHash) && !buildJobMap.containsKey(buildJobQueueItem.id()); }); - hazelcastInstance.getTopic("resumeBuildAgentTopic").publish(memberAddress); + hazelcastInstance.getTopic("resumeBuildAgentTopic").publish(buildAgentName); localVCLocalCITestService.testLatestSubmission(studentParticipation.getId(), commitHash, 1, false); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java index e5c7f52e1413..a4d157d1a690 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.util.LinkedMultiValueMap; @@ -23,6 +24,7 @@ import de.tum.cit.aet.artemis.assessment.domain.AssessmentType; import de.tum.cit.aet.artemis.assessment.domain.Result; +import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentInformation; import de.tum.cit.aet.artemis.buildagent.dto.BuildConfig; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; @@ -67,6 +69,14 @@ protected String getTestPrefix() { return TEST_PREFIX; } + @Value("${artemis.continuous-integration.build-agent.short-name}") + private String buildAgentShortName; + + @Value("${artemis.continuous-integration.build-agent.display-name:}") + private String buildAgentDisplayName; + + private BuildAgentDTO buildAgent; + @BeforeEach void createJobs() { // temporarily remove listener to avoid triggering build job processing @@ -79,17 +89,19 @@ void createJobs() { BuildConfig buildConfig = new BuildConfig("echo 'test'", "test", "test", "test", "test", "test", null, null, false, false, false, null, 0, null, null, null); RepositoryInfo repositoryInfo = new RepositoryInfo("test", null, RepositoryType.USER, "test", "test", "test", null, null); - job1 = new BuildJobQueueItem("1", "job1", "address1", 1, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo1, buildConfig, null); - job2 = new BuildJobQueueItem("2", "job2", "address1", 2, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo2, buildConfig, null); String memberAddress = hazelcastInstance.getCluster().getLocalMember().getAddress().toString(); - agent1 = new BuildAgentInformation(memberAddress, 1, 0, new ArrayList<>(List.of(job1)), BuildAgentInformation.BuildAgentStatus.IDLE, new ArrayList<>(List.of(job2)), null); - BuildJobQueueItem finishedJobQueueItem1 = new BuildJobQueueItem("3", "job3", "address1", 3, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo1, + buildAgent = new BuildAgentDTO(buildAgentShortName, memberAddress, buildAgentDisplayName); + + job1 = new BuildJobQueueItem("1", "job1", buildAgent, 1, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo1, buildConfig, null); + job2 = new BuildJobQueueItem("2", "job2", buildAgent, 2, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo2, buildConfig, null); + agent1 = new BuildAgentInformation(buildAgent, 1, 0, new ArrayList<>(List.of(job1)), BuildAgentInformation.BuildAgentStatus.IDLE, new ArrayList<>(List.of(job2)), null); + BuildJobQueueItem finishedJobQueueItem1 = new BuildJobQueueItem("3", "job3", buildAgent, 3, course.getId(), 1, 1, 1, BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo1, buildConfig, null); - BuildJobQueueItem finishedJobQueueItem2 = new BuildJobQueueItem("4", "job4", "address1", 4, course.getId() + 1, 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo2, + BuildJobQueueItem finishedJobQueueItem2 = new BuildJobQueueItem("4", "job4", buildAgent, 4, course.getId() + 1, 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo2, buildConfig, null); - BuildJobQueueItem finishedJobQueueItem3 = new BuildJobQueueItem("5", "job5", "address1", 5, course.getId() + 2, 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo3, + BuildJobQueueItem finishedJobQueueItem3 = new BuildJobQueueItem("5", "job5", buildAgent, 5, course.getId() + 2, 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo3, buildConfig, null); - BuildJobQueueItem finishedJobQueueItemForLogs = new BuildJobQueueItem("6", "job5", "address1", 5, course.getId(), programmingExercise.getId(), 1, 1, BuildStatus.FAILED, + BuildJobQueueItem finishedJobQueueItemForLogs = new BuildJobQueueItem("6", "job5", buildAgent, 5, course.getId(), programmingExercise.getId(), 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo3, buildConfig, null); var result1 = new Result().successful(true).rated(true).score(100D).assessmentType(AssessmentType.AUTOMATIC).completionDate(ZonedDateTime.now()); var result2 = new Result().successful(false).rated(true).score(0D).assessmentType(AssessmentType.AUTOMATIC).completionDate(ZonedDateTime.now()); @@ -187,8 +199,8 @@ void testGetBuildAgents_returnsAgents() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") void testGetBuildAgentDetails_returnsAgent() throws Exception { - var retrievedAgent = request.get("/api/admin/build-agent?agentName=" + agent1.name(), HttpStatus.OK, BuildAgentInformation.class); - assertThat(retrievedAgent.name()).isEqualTo(agent1.name()); + var retrievedAgent = request.get("/api/admin/build-agent?agentName=" + agent1.buildAgent().name(), HttpStatus.OK, BuildAgentInformation.class); + assertThat(retrievedAgent.buildAgent().name()).isEqualTo(agent1.buildAgent().name()); } @Test @@ -241,7 +253,7 @@ void testCancelAllRunningBuildJobsForCourse() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") void testCancelAllRunningBuildJobsForAgent() throws Exception { - request.delete("/api/admin/cancel-all-running-jobs-for-agent?agentName=" + agent1.name(), HttpStatus.NO_CONTENT); + request.delete("/api/admin/cancel-all-running-jobs-for-agent?agentName=" + agent1.buildAgent().name(), HttpStatus.NO_CONTENT); } @Test @@ -270,7 +282,7 @@ void testGetFinishedBuildJobs_returnsFilteredJobs() throws Exception { ZonedDateTime.now().plusDays(1).plusMinutes(10)); BuildConfig buildConfig = new BuildConfig("echo 'test'", "test", "test", "test", "test", "test", null, null, false, false, false, null, 0, null, null, null); RepositoryInfo repositoryInfo = new RepositoryInfo("test", null, RepositoryType.USER, "test", "test", "test", null, null); - var failedJob1 = new BuildJobQueueItem("5", "job5", "address1", 1, course.getId(), 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo, buildConfig, null); + var failedJob1 = new BuildJobQueueItem("5", "job5", buildAgent, 1, course.getId(), 1, 1, 1, BuildStatus.FAILED, repositoryInfo, jobTimingInfo, buildConfig, null); var jobResult = new Result().successful(false).rated(true).score(0D).assessmentType(AssessmentType.AUTOMATIC).completionDate(ZonedDateTime.now()); var failedFinishedJob = new BuildJob(failedJob1, BuildStatus.FAILED, jobResult); @@ -349,10 +361,13 @@ void testGetBuildJobStatistics() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") void testPauseBuildAgent() throws Exception { - request.put("/api/admin/agent/" + URLEncoder.encode(agent1.name(), StandardCharsets.UTF_8) + "/pause", null, HttpStatus.NO_CONTENT); - await().until(() -> buildAgentInformation.get(agent1.name()).status() == BuildAgentInformation.BuildAgentStatus.PAUSED); + // We need to clear the processing jobs to avoid the agent being set to ACTIVE again + processingJobs.clear(); + + request.put("/api/admin/agent/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/pause", null, HttpStatus.NO_CONTENT); + await().until(() -> buildAgentInformation.get(agent1.buildAgent().memberAddress()).status() == BuildAgentInformation.BuildAgentStatus.PAUSED); - request.put("/api/admin/agent/" + URLEncoder.encode(agent1.name(), StandardCharsets.UTF_8) + "/resume", null, HttpStatus.NO_CONTENT); - await().until(() -> buildAgentInformation.get(agent1.name()).status() == BuildAgentInformation.BuildAgentStatus.IDLE); + request.put("/api/admin/agent/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/resume", null, HttpStatus.NO_CONTENT); + await().until(() -> buildAgentInformation.get(agent1.buildAgent().memberAddress()).status() == BuildAgentInformation.BuildAgentStatus.IDLE); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java index a3fbfd1b2a7b..fe88b498bb47 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIServiceTest.java @@ -20,6 +20,7 @@ import com.hazelcast.collection.IQueue; import com.hazelcast.map.IMap; +import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentDTO; import de.tum.cit.aet.artemis.buildagent.dto.BuildConfig; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; import de.tum.cit.aet.artemis.buildagent.dto.JobTimingInfo; @@ -73,9 +74,12 @@ void testReturnCorrectBuildStatus() { BuildConfig buildConfig = new BuildConfig("echo 'test'", "test", "test", "test", "test", "test", null, null, false, false, false, null, 0, null, null, null); RepositoryInfo repositoryInfo = new RepositoryInfo("test", null, RepositoryType.USER, "test", "test", "test", null, null); - BuildJobQueueItem job1 = new BuildJobQueueItem("1", "job1", "address1", participation.getId(), course.getId(), 1, 1, 1, + String memberAddress = hazelcastInstance.getCluster().getLocalMember().getAddress().toString(); + BuildAgentDTO buildAgent = new BuildAgentDTO("artemis-build-agent-test", memberAddress, "artemis-build-agent-test"); + + BuildJobQueueItem job1 = new BuildJobQueueItem("1", "job1", buildAgent, participation.getId(), course.getId(), 1, 1, 1, de.tum.cit.aet.artemis.programming.domain.build.BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo, buildConfig, null); - BuildJobQueueItem job2 = new BuildJobQueueItem("2", "job2", "address1", participation.getId(), course.getId(), 1, 1, 1, + BuildJobQueueItem job2 = new BuildJobQueueItem("2", "job2", buildAgent, participation.getId(), course.getId(), 1, 1, 1, de.tum.cit.aet.artemis.programming.domain.build.BuildStatus.SUCCESSFUL, repositoryInfo, jobTimingInfo, buildConfig, null); queuedJobs = hazelcastInstance.getQueue("buildJobQueue"); diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts index 2db5c31ac80b..0b14d136c97c 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agent-details.component.spec.ts @@ -9,7 +9,7 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { DataTableComponent } from 'app/shared/data-table/data-table.component'; import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; import { NgxDatatableModule } from '@siemens/ngx-datatable'; -import { BuildAgent, BuildAgentStatus } from 'app/entities/programming/build-agent.model'; +import { BuildAgentInformation, BuildAgentStatus } from '../../../../../../main/webapp/app/entities/programming/build-agent-information.model'; import { RepositoryInfo, TriggeredByPushTo } from 'app/entities/programming/repository-info.model'; import { JobTimingInfo } from 'app/entities/job-timing-info.model'; import { BuildConfig } from 'app/entities/programming/build-config.model'; @@ -69,7 +69,7 @@ describe('BuildAgentDetailsComponent', () => { { id: '2', name: 'Build Job 2', - buildAgentAddress: 'agent2', + buildAgent: { name: 'agent2', memberAddress: 'localhost:8080', displayName: 'Agent 2' }, participationId: 102, courseId: 10, exerciseId: 100, @@ -82,7 +82,7 @@ describe('BuildAgentDetailsComponent', () => { { id: '4', name: 'Build Job 4', - buildAgentAddress: 'agent4', + buildAgent: { name: 'agent4', memberAddress: 'localhost:8080', displayName: 'Agent 4' }, participationId: 104, courseId: 10, exerciseId: 100, @@ -98,7 +98,7 @@ describe('BuildAgentDetailsComponent', () => { { id: '1', name: 'Build Job 1', - buildAgentAddress: 'agent1', + buildAgent: { name: 'agent1', memberAddress: 'localhost:8080', displayName: 'Agent 1' }, participationId: 101, courseId: 10, exerciseId: 100, @@ -110,9 +110,9 @@ describe('BuildAgentDetailsComponent', () => { }, ]; - const mockBuildAgent: BuildAgent = { + const mockBuildAgent: BuildAgentInformation = { id: 1, - name: 'buildagent1', + buildAgent: { name: 'agent1', memberAddress: 'localhost:8080', displayName: 'Agent 1' }, maxNumberOfConcurrentBuildJobs: 2, numberOfCurrentBuildJobs: 2, runningBuildJobs: mockRunningJobs1, @@ -139,7 +139,7 @@ describe('BuildAgentDetailsComponent', () => { fixture = TestBed.createComponent(BuildAgentDetailsComponent); component = fixture.componentInstance; activatedRoute = fixture.debugElement.injector.get(ActivatedRoute) as MockActivatedRoute; - activatedRoute.setParameters({ agentName: mockBuildAgent.name }); + activatedRoute.setParameters({ agentName: mockBuildAgent.buildAgent?.name }); alertService = TestBed.inject(AlertService); alertServiceAddAlertStub = jest.spyOn(alertService, 'addAlert'); })); @@ -164,8 +164,8 @@ describe('BuildAgentDetailsComponent', () => { component.ngOnInit(); expect(component.buildAgent).toEqual(mockBuildAgent); - expect(mockWebsocketService.subscribe).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.name); - expect(mockWebsocketService.receive).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.name); + expect(mockWebsocketService.subscribe).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.buildAgent?.name); + expect(mockWebsocketService.receive).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.buildAgent?.name); }); it('should unsubscribe from the websocket channel on destruction', () => { @@ -175,7 +175,7 @@ describe('BuildAgentDetailsComponent', () => { component.ngOnDestroy(); - expect(mockWebsocketService.unsubscribe).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.name); + expect(mockWebsocketService.unsubscribe).toHaveBeenCalledWith('/topic/admin/build-agent/' + component.buildAgent.buildAgent?.name); }); it('should set recent build jobs duration', () => { @@ -213,7 +213,7 @@ describe('BuildAgentDetailsComponent', () => { }); it('should show an alert when pausing build agent without a name', () => { - component.buildAgent = { ...mockBuildAgent, name: '' }; + component.buildAgent = { ...mockBuildAgent, buildAgent: { ...mockBuildAgent.buildAgent, name: '' } }; component.pauseBuildAgent(); expect(alertServiceAddAlertStub).toHaveBeenCalledWith({ @@ -223,7 +223,7 @@ describe('BuildAgentDetailsComponent', () => { }); it('should show an alert when resuming build agent without a name', () => { - component.buildAgent = { ...mockBuildAgent, name: '' }; + component.buildAgent = { ...mockBuildAgent, buildAgent: { ...mockBuildAgent.buildAgent, name: '' } }; component.resumeBuildAgent(); expect(alertServiceAddAlertStub).toHaveBeenCalledWith({ diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts index 9a4e94b1cb7e..fe898f69474a 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts @@ -10,7 +10,7 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { DataTableComponent } from 'app/shared/data-table/data-table.component'; import { MockComponent, MockPipe } from 'ng-mocks'; import { NgxDatatableModule } from '@siemens/ngx-datatable'; -import { BuildAgent, BuildAgentStatus } from 'app/entities/programming/build-agent.model'; +import { BuildAgentInformation, BuildAgentStatus } from '../../../../../../main/webapp/app/entities/programming/build-agent-information.model'; import { RepositoryInfo, TriggeredByPushTo } from 'app/entities/programming/repository-info.model'; import { JobTimingInfo } from 'app/entities/job-timing-info.model'; import { BuildConfig } from 'app/entities/programming/build-config.model'; @@ -63,7 +63,7 @@ describe('BuildAgentSummaryComponent', () => { { id: '2', name: 'Build Job 2', - buildAgentAddress: 'agent2', + buildAgent: { name: 'agent2', memberAddress: 'localhost:8080', displayName: 'Agent 2' }, participationId: 102, courseId: 10, exerciseId: 100, @@ -76,7 +76,7 @@ describe('BuildAgentSummaryComponent', () => { { id: '4', name: 'Build Job 4', - buildAgentAddress: 'agent4', + buildAgent: { name: 'agent4', memberAddress: 'localhost:8080', displayName: 'Agent 4' }, participationId: 104, courseId: 10, exerciseId: 100, @@ -92,7 +92,7 @@ describe('BuildAgentSummaryComponent', () => { { id: '1', name: 'Build Job 1', - buildAgentAddress: 'agent1', + buildAgent: { name: 'agent1', memberAddress: 'localhost:8080', displayName: 'Agent 1' }, participationId: 101, courseId: 10, exerciseId: 100, @@ -105,7 +105,7 @@ describe('BuildAgentSummaryComponent', () => { { id: '3', name: 'Build Job 3', - buildAgentAddress: 'agent3', + buildAgent: { name: 'agent3', memberAddress: 'localhost:8080', displayName: 'Agent 3' }, participationId: 103, courseId: 10, exerciseId: 100, @@ -117,10 +117,10 @@ describe('BuildAgentSummaryComponent', () => { }, ]; - const mockBuildAgents: BuildAgent[] = [ + const mockBuildAgents: BuildAgentInformation[] = [ { id: 1, - name: 'buildagent1', + buildAgent: { name: 'buildagent1', displayName: 'Build Agent 1', memberAddress: 'agent1' }, maxNumberOfConcurrentBuildJobs: 2, numberOfCurrentBuildJobs: 2, runningBuildJobs: mockRunningJobs1, @@ -128,7 +128,7 @@ describe('BuildAgentSummaryComponent', () => { }, { id: 2, - name: 'buildagent2', + buildAgent: { name: 'buildagent2', displayName: 'Build Agent 2', memberAddress: 'agent2' }, maxNumberOfConcurrentBuildJobs: 2, numberOfCurrentBuildJobs: 2, runningBuildJobs: mockRunningJobs2, @@ -196,9 +196,9 @@ describe('BuildAgentSummaryComponent', () => { const spy = jest.spyOn(component, 'cancelAllBuildJobs'); component.ngOnInit(); - component.cancelAllBuildJobs(buildAgent.name!); + component.cancelAllBuildJobs(buildAgent.buildAgent); - expect(spy).toHaveBeenCalledExactlyOnceWith(buildAgent.name!); + expect(spy).toHaveBeenCalledExactlyOnceWith(buildAgent.buildAgent); }); it('should calculate the build capacity and current builds', () => { diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts index c73f5d49ee15..261acf610ff2 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts @@ -8,7 +8,7 @@ import { BuildJob } from 'app/entities/programming/build-job.model'; import dayjs from 'dayjs/esm'; import { lastValueFrom } from 'rxjs'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; -import { BuildAgent } from 'app/entities/programming/build-agent.model'; +import { BuildAgentInformation } from '../../../../../../main/webapp/app/entities/programming/build-agent-information.model'; import { RepositoryInfo, TriggeredByPushTo } from 'app/entities/programming/repository-info.model'; import { JobTimingInfo } from 'app/entities/job-timing-info.model'; import { BuildConfig } from 'app/entities/programming/build-config.model'; @@ -16,7 +16,7 @@ import { BuildConfig } from 'app/entities/programming/build-config.model'; describe('BuildAgentsService', () => { let service: BuildAgentsService; let httpMock: HttpTestingController; - let element: BuildAgent; + let element: BuildAgentInformation; const repositoryInfo: RepositoryInfo = { repositoryName: 'repo2', @@ -50,7 +50,7 @@ describe('BuildAgentsService', () => { { id: '2', name: 'Build Job 2', - buildAgentAddress: 'agent2', + buildAgent: { name: 'agent2', memberAddress: 'localhost:8080', displayName: 'Agent 2' }, participationId: 102, courseId: 10, exerciseId: 100, @@ -63,7 +63,7 @@ describe('BuildAgentsService', () => { { id: '4', name: 'Build Job 4', - buildAgentAddress: 'agent4', + buildAgent: { name: 'agent4', memberAddress: 'localhost:8080', displayName: 'Agent 4' }, participationId: 104, courseId: 10, exerciseId: 100, @@ -82,9 +82,9 @@ describe('BuildAgentsService', () => { }); service = TestBed.inject(BuildAgentsService); httpMock = TestBed.inject(HttpTestingController); - element = new BuildAgent(); + element = new BuildAgentInformation(); element.id = 1; - element.name = 'BuildAgent1'; + element.buildAgent = { name: 'buildAgent1', memberAddress: 'localhost:8080', displayName: 'Build Agent 1' }; element.maxNumberOfConcurrentBuildJobs = 3; element.numberOfCurrentBuildJobs = 1; element.runningBuildJobs = mockRunningJobs; diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index 484e5f300ba1..efe58275f415 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -76,6 +76,8 @@ artemis: default: "~~invalid~~" typescript: default: "~~invalid~~" + build-agent: + short-name: "artemis-build-agent-test" spring: application: From 0745f27163367852bfac2a89a8a813731e742aff Mon Sep 17 00:00:00 2001 From: Michael Dyer <59163924+MichaelOwenDyer@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:36:54 +0200 Subject: [PATCH 12/18] Development: Fix exercise deletion with existing Iris sessions (#9567) --- .../changelog/20241023456789_changelog.xml | 25 +++++++++++++++++++ .../resources/config/liquibase/master.xml | 1 + ...risExerciseChatSessionIntegrationTest.java | 13 +++++++++- 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/config/liquibase/changelog/20241023456789_changelog.xml diff --git a/src/main/resources/config/liquibase/changelog/20241023456789_changelog.xml b/src/main/resources/config/liquibase/changelog/20241023456789_changelog.xml new file mode 100644 index 000000000000..8606c28d3bee --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241023456789_changelog.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 109eefaa1bbf..5a2444f8722d 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -29,6 +29,7 @@ + diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/IrisExerciseChatSessionIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/IrisExerciseChatSessionIntegrationTest.java index 38a613e24379..aeaa3fee33bc 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/IrisExerciseChatSessionIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/IrisExerciseChatSessionIntegrationTest.java @@ -28,7 +28,7 @@ class IrisExerciseChatSessionIntegrationTest extends AbstractIrisIntegrationTest @BeforeEach void initTestCase() { - userUtilService.addUsers(TEST_PREFIX, 1, 0, 0, 0); + userUtilService.addUsers(TEST_PREFIX, 1, 0, 0, 1); final Course course = programmingExerciseUtilService.addCourseWithOneProgrammingExerciseAndTestCases(); exercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); @@ -90,6 +90,17 @@ void irisStatus() throws Exception { assertThat(request.get("/api/iris/status", HttpStatus.OK, IrisStatusDTO.class).active()).isFalse(); } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testDeleteExerciseWithIrisSession() throws Exception { + var irisSession = request.postWithResponseBody(exerciseChatUrl(exercise.getId()), null, IrisSession.class, HttpStatus.CREATED); + assertThat(irisExerciseChatSessionRepository.findByIdElseThrow(irisSession.getId())).isNotNull(); + // Set the URL request parameters to prevent an internal server error which is irrelevant for this test + var url = "/api/programming-exercises/" + exercise.getId() + "?deleteStudentReposBuildPlans=false&deleteBaseReposBuildPlans=false"; + request.delete(url, HttpStatus.OK); + assertThat(irisExerciseChatSessionRepository.findById(irisSession.getId())).isEmpty(); + } + private static String exerciseChatUrl(long sessionId) { return "/api/iris/exercise-chat/" + sessionId + "/sessions"; } From 48a5b4304af5c0c824c94cf73994a9a85ac4b7b2 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Thu, 24 Oct 2024 08:38:21 +0200 Subject: [PATCH 13/18] General: Improve user administration (#9533) --- .../user-management-update.component.html | 18 ++++------- .../user-management-update.component.ts | 12 ++++--- .../user-management.component.html | 6 ++-- .../user-management.component.ts | 32 ++++++++++--------- .../user-management/user-management.route.ts | 2 +- src/main/webapp/i18n/de/user-management.json | 4 +-- src/main/webapp/i18n/en/user-management.json | 4 +-- .../admin/user-management.component.spec.ts | 22 +++++++------ 8 files changed, 53 insertions(+), 47 deletions(-) diff --git a/src/main/webapp/app/admin/user-management/user-management-update.component.html b/src/main/webapp/app/admin/user-management/user-management-update.component.html index 71f70e3752db..bc55e3a13acb 100644 --- a/src/main/webapp/app/admin/user-management/user-management-update.component.html +++ b/src/main/webapp/app/admin/user-management/user-management-update.component.html @@ -1,7 +1,11 @@
    -

    + @if (user.id === undefined) { +

    + } @else { +

    + }
    @@ -83,7 +87,7 @@

    @@ -255,15 +259,7 @@

    diff --git a/src/main/webapp/app/admin/user-management/user-management-update.component.ts b/src/main/webapp/app/admin/user-management/user-management-update.component.ts index 2c4760d74f09..2d7e318f9038 100644 --- a/src/main/webapp/app/admin/user-management/user-management-update.component.ts +++ b/src/main/webapp/app/admin/user-management/user-management-update.component.ts @@ -16,7 +16,6 @@ import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { AlertService, AlertType } from 'app/core/util/alert.service'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { AdminUserService } from 'app/core/user/admin-user.service'; -import { CourseManagementService } from 'app/course/manage/course-management.service'; import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; import { CourseAdminService } from 'app/course/manage/course-admin.service'; @@ -59,7 +58,6 @@ export class UserManagementUpdateComponent implements OnInit { constructor( private languageHelper: JhiLanguageHelper, private userService: AdminUserService, - private courseManagementService: CourseManagementService, private courseAdminService: CourseAdminService, private route: ActivatedRoute, private organizationService: OrganizationManagementService, @@ -232,11 +230,17 @@ export class UserManagementUpdateComponent implements OnInit { passwordInput: ['', [Validators.minLength(PASSWORD_MIN_LENGTH), Validators.maxLength(PASSWORD_MAX_LENGTH)]], emailInput: ['', [Validators.required, Validators.minLength(this.EMAIL_MIN_LENGTH), Validators.maxLength(this.EMAIL_MAX_LENGTH)]], registrationNumberInput: ['', [Validators.maxLength(this.REGISTRATION_NUMBER_MAX_LENGTH)]], - activatedInput: ['', []], + activatedInput: [{ value: this.user.activated }], langKeyInput: ['', []], authorityInput: ['', []], - internalInput: [{ value: this.user.internal, disabled: true }], + internalInput: [{ value: this.user.internal, disabled: true }], // initially disabled, will be enabled if user.id is undefined }); + // Conditionally enable or disable 'internalInput' based on user.id + if (this.user.id !== undefined) { + this.editForm.get('internalInput')?.disable(); // Artemis does not support to edit the internal flag for existing users + } else { + this.editForm.get('internalInput')?.enable(); // New users can either be internal or external + } } /** diff --git a/src/main/webapp/app/admin/user-management/user-management.component.html b/src/main/webapp/app/admin/user-management/user-management.component.html index 129f30d67feb..c8f49ab96a2e 100644 --- a/src/main/webapp/app/admin/user-management/user-management.component.html +++ b/src/main/webapp/app/admin/user-management/user-management.component.html @@ -27,13 +27,13 @@

    name="searchTerm" id="field_searchTerm" formControlName="searchControl" - [(ngModel)]="searchTerm" - (focusout)="loadAll()" + (blur)="loadAll()" + (keydown)="onKeydown($event)" /> - @if (searchControl.invalid && (searchControl.dirty || searchControl.touched)) { + @if (searchInvalid) {
    diff --git a/src/main/webapp/app/admin/user-management/user-management.component.ts b/src/main/webapp/app/admin/user-management/user-management.component.ts index 5ce57b8d4ed1..f7870f5369cf 100644 --- a/src/main/webapp/app/admin/user-management/user-management.component.ts +++ b/src/main/webapp/app/admin/user-management/user-management.component.ts @@ -7,8 +7,8 @@ import { User } from 'app/core/user/user.model'; import { AccountService } from 'app/core/auth/account.service'; import { AlertService } from 'app/core/util/alert.service'; import { SortingOrder } from 'app/shared/table/pageable-table'; -import { debounceTime, switchMap, tap } from 'rxjs/operators'; -import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; +import { switchMap, tap } from 'rxjs/operators'; +import { FormControl, FormGroup } from '@angular/forms'; import { EventManager } from 'app/core/util/event-manager.service'; import { ASC, DESC, ITEMS_PER_PAGE, SORT } from 'app/shared/constants/pagination.constants'; import { faEye, faFilter, faPlus, faSort, faTimes, faWrench } from '@fortawesome/free-solid-svg-icons'; @@ -17,7 +17,6 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { AdminUserService } from 'app/core/user/admin-user.service'; -import { UserService } from 'app/core/user/user.service'; export class UserFilter { authorityFilter: Set = new Set(); @@ -103,6 +102,7 @@ export class UserManagementComponent implements OnInit, OnDestroy { predicate!: string; ascending!: boolean; searchTermString = ''; + searchInvalid = false; isLdapProfileActive: boolean; // filters @@ -129,7 +129,6 @@ export class UserManagementComponent implements OnInit, OnDestroy { constructor( private adminUserService: AdminUserService, - private userService: UserService, private alertService: AlertService, private accountService: AccountService, private activatedRoute: ActivatedRoute, @@ -148,7 +147,6 @@ export class UserManagementComponent implements OnInit, OnDestroy { this.search .pipe( tap(() => (this.loadingSearchResult = true)), - debounceTime(1000), switchMap(() => this.adminUserService.query( { @@ -175,7 +173,7 @@ export class UserManagementComponent implements OnInit, OnDestroy { }); this.userSearchForm = new FormGroup({ - searchControl: new FormControl('', { validators: [this.validateUserSearch], updateOn: 'blur' }), + searchControl: new FormControl('', { updateOn: 'change' }), }); this.accountService.identity().then((user) => { this.currentAccount = user!; @@ -443,17 +441,21 @@ export class UserManagementComponent implements OnInit, OnDestroy { * Retrieve the list of users from the user service for a single page in the user management based on the page, size and sort configuration */ loadAll() { + this.searchTerm = this.searchControl.value; if (this.searchTerm.length >= 3 || this.searchTerm.length === 0) { + this.searchInvalid = false; this.search.next(); + } else { + this.searchInvalid = true; } } /** * Returns the unique identifier for items in the collection - * @param index of a user in the collection + * @param _index of a user in the collection * @param item current user */ - trackIdentity(index: number, item: User) { + trackIdentity(_index: number, item: User) { return item.id ?? -1; } @@ -520,14 +522,14 @@ export class UserManagementComponent implements OnInit, OnDestroy { return this.searchTermString; } - validateUserSearch(control: AbstractControl) { - if (control.value.length >= 1 && control.value.length <= 2) { - return { searchControl: true }; - } - return null; - } - get searchControl() { return this.userSearchForm.get('searchControl')!; } + + onKeydown(event: KeyboardEvent) { + if (event.key === 'Enter') { + event.preventDefault(); // Prevent the default form submission behavior + this.loadAll(); // Trigger the search logic + } + } } diff --git a/src/main/webapp/app/admin/user-management/user-management.route.ts b/src/main/webapp/app/admin/user-management/user-management.route.ts index e98972a5c3f0..df2178c4a68b 100644 --- a/src/main/webapp/app/admin/user-management/user-management.route.ts +++ b/src/main/webapp/app/admin/user-management/user-management.route.ts @@ -51,7 +51,7 @@ export const userManagementRoute: Route[] = [ path: 'edit', component: UserManagementUpdateComponent, data: { - pageTitle: 'artemisApp.userManagement.home.createOrEditLabel', + pageTitle: 'artemisApp.userManagement.home.editLabel', }, }, ], diff --git a/src/main/webapp/i18n/de/user-management.json b/src/main/webapp/i18n/de/user-management.json index 365abf875684..6be2470bd4fd 100644 --- a/src/main/webapp/i18n/de/user-management.json +++ b/src/main/webapp/i18n/de/user-management.json @@ -64,7 +64,7 @@ "home": { "title": "Nutzer:in", "createLabel": "Nutzer:in erstellen", - "createOrEditLabel": "Nutzer:in erstellen oder bearbeiten" + "editLabel": "Nutzer:in bearbeiten" }, "created": "Nutzer:in wurde mit ID {{ param }} erstellt", "updated": "Nutzer:in mit ID {{ param }} wurde geändert", @@ -93,7 +93,7 @@ "profiles": "Profile", "langKey": "Sprache", "internal": "Intern", - "passwordTooltip": "Du kannst nur Passwörter für interne Nutzer:innen ändern.", + "passwordTooltip": "Du kannst diese Einstellung nur für neue Nutzer:innen ändern. Bitte beachte, dass du nur Passwörter für interne Nutzer:innen ändern kannst.", "createdBy": "Erstellt von", "createdDate": "Erstellt am", "lastModifiedBy": "Bearbeitet von", diff --git a/src/main/webapp/i18n/en/user-management.json b/src/main/webapp/i18n/en/user-management.json index 689da224ef2d..f730d94045d4 100644 --- a/src/main/webapp/i18n/en/user-management.json +++ b/src/main/webapp/i18n/en/user-management.json @@ -64,7 +64,7 @@ "home": { "title": "Users", "createLabel": "Create a new user", - "createOrEditLabel": "Create or edit a user" + "editLabel": "Edit user" }, "created": "Created new user with identifier {{ param }}", "updated": "Updated User with identifier {{ param }}", @@ -92,7 +92,7 @@ "deactivated": "Deactivated", "profiles": "Profiles", "langKey": "Language", - "passwordTooltip": "You can only change passwords for internal users.", + "passwordTooltip": "This setting can only be changed for new users. Notice, that you can only change passwords for internal users.", "internal": "Internal", "createdBy": "Created by", "createdDate": "Created date", diff --git a/src/test/javascript/spec/component/admin/user-management.component.spec.ts b/src/test/javascript/spec/component/admin/user-management.component.spec.ts index 4b2ee62e13a4..a4c5dda88562 100644 --- a/src/test/javascript/spec/component/admin/user-management.component.spec.ts +++ b/src/test/javascript/spec/component/admin/user-management.component.spec.ts @@ -14,7 +14,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { HttpHeaders, HttpParams, HttpResponse, provideHttpClient } from '@angular/common/http'; import { User } from 'app/core/user/user.model'; import { Subscription, of } from 'rxjs'; -import { AbstractControl, ReactiveFormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; import { ItemCountComponent } from 'app/shared/pagination/item-count.component'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; @@ -168,7 +168,7 @@ describe('UserManagementComponent', () => { // THEN expect(userService.update).toHaveBeenCalledWith({ ...user, activated: true }); - expect(userService.query).toHaveBeenCalledOnce(); + expect(userService.query).toHaveBeenCalledTimes(2); expect(comp.users && comp.users[0]).toEqual(expect.objectContaining({ id: 123 })); expect(profileSpy).toHaveBeenCalledOnce(); }), @@ -243,14 +243,17 @@ describe('UserManagementComponent', () => { jest.restoreAllMocks(); }); - it('should validate user search correctly', () => { - expect(comp.validateUserSearch({ value: [] } as AbstractControl)).toBeNull(); - expect(comp.validateUserSearch({ value: [0] } as AbstractControl)).toEqual({ searchControl: true }); - expect(comp.validateUserSearch({ value: [0, 0] } as AbstractControl)).toEqual({ searchControl: true }); - expect(comp.validateUserSearch({ value: [0, 0, 0] } as AbstractControl)).toBeNull(); - }); - it('should call initFilters', () => { + const headers = new HttpHeaders().append('link', 'link;link'); + const user = new User(123); + jest.spyOn(userService, 'query').mockReturnValue( + of( + new HttpResponse({ + body: [user], + headers, + }), + ), + ); const spy = jest.spyOn(comp, 'initFilters'); const initSpy = jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(new ProfileInfo())); @@ -258,6 +261,7 @@ describe('UserManagementComponent', () => { expect(spy).toHaveBeenCalledOnce(); expect(initSpy).toHaveBeenCalledOnce(); + expect(userService.query).toHaveBeenCalledTimes(0); }); it.each` From 6f5081a3beb5f1446c27c740245700f6f2ebff3c Mon Sep 17 00:00:00 2001 From: Paul Rangger <48455539+PaRangger@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:38:50 +0200 Subject: [PATCH 14/18] General: Add profile pictures to course user list and user administration (#9553) --- src/main/webapp/app/admin/admin.module.ts | 2 + .../user-management.component.html | 15 ++++++ .../tutorial-group-detail.component.html | 17 +++--- .../tutorial-group-detail.component.scss | 19 ------- .../tutorial-group-detail.component.ts | 6 --- .../shared/tutorial-groups-shared.module.ts | 3 +- .../course-conversations.module.ts | 2 + .../conversation-member-row.component.html | 20 +++---- .../conversation-member-row.component.scss | 19 ------- .../conversation-member-row.component.ts | 10 ++-- .../course-group/course-group.component.html | 19 +++++++ .../course-group/course-group.module.ts | 3 +- .../app/shared/metis/metis.component.scss | 46 ---------------- .../webapp/app/shared/metis/metis.module.ts | 2 + .../answer-post-header.component.html | 40 ++++---------- .../post-header/post-header.component.html | 40 ++++---------- .../posting-header.directive.ts | 6 --- .../profile-picture.component.html | 25 +++++++++ .../profile-picture.component.scss | 29 +++++++++++ .../profile-picture.component.ts | 50 ++++++++++++++++++ src/main/webapp/i18n/de/course.json | 3 +- src/main/webapp/i18n/de/user-management.json | 3 +- src/main/webapp/i18n/en/course.json | 3 +- src/main/webapp/i18n/en/user-management.json | 3 +- .../conversation-member-row.component.spec.ts | 9 +--- .../discussion-section.component.spec.ts | 3 +- .../answer-post-header.component.spec.ts | 7 +-- .../post-header/post-header.component.spec.ts | 7 +-- .../profile-picture.component.spec.ts | 52 +++++++++++++++++++ .../tutorial-group-detail.component.spec.ts | 3 ++ 30 files changed, 265 insertions(+), 201 deletions(-) create mode 100644 src/main/webapp/app/shared/profile-picture/profile-picture.component.html create mode 100644 src/main/webapp/app/shared/profile-picture/profile-picture.component.scss create mode 100644 src/main/webapp/app/shared/profile-picture/profile-picture.component.ts create mode 100644 src/test/javascript/spec/component/shared/profile-picture/profile-picture.component.spec.ts diff --git a/src/main/webapp/app/admin/admin.module.ts b/src/main/webapp/app/admin/admin.module.ts index 92118d36ea07..28283dd58512 100644 --- a/src/main/webapp/app/admin/admin.module.ts +++ b/src/main/webapp/app/admin/admin.module.ts @@ -47,6 +47,7 @@ import { KnowledgeAreaTreeComponent } from 'app/shared/standardized-competencies import { StandardizedCompetencyFilterComponent } from 'app/shared/standardized-competencies/standardized-competency-filter.component'; import { StandardizedCompetencyDetailComponent } from 'app/shared/standardized-competencies/standardized-competency-detail.component'; import { DeleteUsersButtonComponent } from 'app/admin/user-management/delete-users-button.component'; +import { ProfilePictureComponent } from 'app/shared/profile-picture/profile-picture.component'; const ENTITY_STATES = [...adminState]; @@ -73,6 +74,7 @@ const ENTITY_STATES = [...adminState]; StandardizedCompetencyFilterComponent, StandardizedCompetencyDetailComponent, DeleteUsersButtonComponent, + ProfilePictureComponent, ], declarations: [ AuditsComponent, diff --git a/src/main/webapp/app/admin/user-management/user-management.component.html b/src/main/webapp/app/admin/user-management/user-management.component.html index c8f49ab96a2e..de3a8d206669 100644 --- a/src/main/webapp/app/admin/user-management/user-management.component.html +++ b/src/main/webapp/app/admin/user-management/user-management.component.html @@ -91,6 +91,9 @@

    + + + @@ -147,6 +150,18 @@

    {{ user.id }} + + + + diff --git a/src/main/webapp/app/course/tutorial-groups/shared/tutorial-group-detail/tutorial-group-detail.component.html b/src/main/webapp/app/course/tutorial-groups/shared/tutorial-group-detail/tutorial-group-detail.component.html index 26607ec88597..aa3e591fcb37 100644 --- a/src/main/webapp/app/course/tutorial-groups/shared/tutorial-group-detail/tutorial-group-detail.component.html +++ b/src/main/webapp/app/course/tutorial-groups/shared/tutorial-group-detail/tutorial-group-detail.component.html @@ -10,13 +10,16 @@

    {{ tutorialGroup.title }}

    - @if (tutorialGroup.teachingAssistantImageUrl) { - - } @else { - {{ - tutorInitials - }} - } + +
    diff --git a/src/main/webapp/app/course/tutorial-groups/shared/tutorial-group-detail/tutorial-group-detail.component.scss b/src/main/webapp/app/course/tutorial-groups/shared/tutorial-group-detail/tutorial-group-detail.component.scss index 0a2501554636..3e484a84165b 100644 --- a/src/main/webapp/app/course/tutorial-groups/shared/tutorial-group-detail/tutorial-group-detail.component.scss +++ b/src/main/webapp/app/course/tutorial-groups/shared/tutorial-group-detail/tutorial-group-detail.component.scss @@ -1,5 +1,3 @@ -$tutor-image-size: 4.5rem; - .tutorial-group-detail { .scrollbar { position: relative; @@ -15,20 +13,3 @@ $tutor-image-size: 4.5rem; margin-bottom: -1rem; } } - -.tutorial-group-detail-tutor-image { - width: $tutor-image-size; - height: $tutor-image-size; - object-fit: cover; -} - -.tutorial-group-detail-tutor-default-image { - width: $tutor-image-size; - height: $tutor-image-size; - font-size: 2rem; - display: inline-flex; - align-items: center; - justify-content: center; - background-color: var(--gray-400); - color: var(--white); -} diff --git a/src/main/webapp/app/course/tutorial-groups/shared/tutorial-group-detail/tutorial-group-detail.component.ts b/src/main/webapp/app/course/tutorial-groups/shared/tutorial-group-detail/tutorial-group-detail.component.ts index 2d2624ac55a4..2b3adc91f4e3 100644 --- a/src/main/webapp/app/course/tutorial-groups/shared/tutorial-group-detail/tutorial-group-detail.component.ts +++ b/src/main/webapp/app/course/tutorial-groups/shared/tutorial-group-detail/tutorial-group-detail.component.ts @@ -9,8 +9,6 @@ import { TranslateService } from '@ngx-translate/core'; import { faCircle, faCircleInfo, faCircleXmark, faPercent, faQuestionCircle, faUserCheck } from '@fortawesome/free-solid-svg-icons'; import dayjs from 'dayjs/esm'; import { SortService } from 'app/shared/service/sort.service'; -import { getInitialsFromString } from 'app/utils/text.utils'; -import { getBackgroundColorHue } from 'app/utils/color.utils'; @Component({ selector: 'jhi-tutorial-group-detail', @@ -34,9 +32,7 @@ export class TutorialGroupDetailComponent implements OnChanges { sessions: TutorialGroupSession[] = []; - tutorInitials: string; tutorialTimeslotString: string | undefined; - tutorDefaultProfilePictureHue: string; isMessagingEnabled: boolean; utilization: number | undefined; @@ -91,8 +87,6 @@ export class TutorialGroupDetailComponent implements OnChanges { getTutorialDetail() { const tutorialGroup = this.tutorialGroup; - this.tutorDefaultProfilePictureHue = getBackgroundColorHue(tutorialGroup.teachingAssistantId ? tutorialGroup.teachingAssistantId.toString() : 'default'); - this.tutorInitials = getInitialsFromString(tutorialGroup.teachingAssistantName ?? 'NA'); this.isMessagingEnabled = isMessagingEnabled(this.course); if (tutorialGroup.averageAttendance && tutorialGroup.capacity) { this.utilization = Math.round((tutorialGroup.averageAttendance / tutorialGroup.capacity) * 100); diff --git a/src/main/webapp/app/course/tutorial-groups/shared/tutorial-groups-shared.module.ts b/src/main/webapp/app/course/tutorial-groups/shared/tutorial-groups-shared.module.ts index 072ea574195e..3c1417c6a2b6 100644 --- a/src/main/webapp/app/course/tutorial-groups/shared/tutorial-groups-shared.module.ts +++ b/src/main/webapp/app/course/tutorial-groups/shared/tutorial-groups-shared.module.ts @@ -14,9 +14,10 @@ import { RemoveSecondsPipe } from 'app/course/tutorial-groups/shared/remove-seco import { MeetingPatternPipe } from 'app/course/tutorial-groups/shared/meeting-pattern.pipe'; import { DetailModule } from 'app/detail-overview-list/detail.module'; import { IconCardComponent } from 'app/shared/icon-card/icon-card.component'; +import { ProfilePictureComponent } from 'app/shared/profile-picture/profile-picture.component'; @NgModule({ - imports: [ArtemisSharedModule, RouterModule, ArtemisSidePanelModule, VerticalProgressBarModule, DetailModule, IconCardComponent], + imports: [ArtemisSharedModule, RouterModule, ArtemisSidePanelModule, VerticalProgressBarModule, DetailModule, IconCardComponent, ProfilePictureComponent], declarations: [ TutorialGroupsTableComponent, TutorialGroupDetailComponent, diff --git a/src/main/webapp/app/overview/course-conversations/course-conversations.module.ts b/src/main/webapp/app/overview/course-conversations/course-conversations.module.ts index 0d593728c9f2..2f6e01364f58 100644 --- a/src/main/webapp/app/overview/course-conversations/course-conversations.module.ts +++ b/src/main/webapp/app/overview/course-conversations/course-conversations.module.ts @@ -31,6 +31,7 @@ import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; import { CourseConversationsCodeOfConductComponent } from 'app/overview/course-conversations/code-of-conduct/course-conversations-code-of-conduct.component'; import { CourseWideSearchComponent } from 'app/overview/course-conversations/course-wide-search/course-wide-search.component'; import { ArtemisSidebarModule } from 'app/shared/sidebar/sidebar.module'; +import { ProfilePictureComponent } from 'app/shared/profile-picture/profile-picture.component'; const routes: Routes = [ { @@ -54,6 +55,7 @@ const routes: Routes = [ ArtemisSidebarModule, InfiniteScrollModule, CourseUsersSelectorModule, + ProfilePictureComponent, ], declarations: [ CourseConversationsComponent, diff --git a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.html b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.html index 47eff1257e65..34454b54a61c 100644 --- a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.html +++ b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.html @@ -1,16 +1,16 @@ @if (activeConversation && course) {
    - @if (userImageUrl) { - - } @else { - {{ userInitials }} - } + + @if (isChannel(activeConversation) && conversationMember?.isChannelModerator) { } diff --git a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.scss b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.scss index a2f66745bed6..28814b8391f5 100644 --- a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.scss +++ b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.scss @@ -1,5 +1,3 @@ -$profile-picture-height: 2rem; - .conversation-member-row { min-height: 3rem; @@ -16,21 +14,4 @@ $profile-picture-height: 2rem; .dropdown-toggle::after { content: none; } - - .conversation-member-row-default-profile-picture { - font-size: 0.8rem; - display: inline-flex; - align-items: center; - justify-content: center; - } - - .conversation-member-row-profile-picture, - .conversation-member-row-default-profile-picture { - width: $profile-picture-height; - height: $profile-picture-height; - max-width: $profile-picture-height; - max-height: $profile-picture-height; - background-color: var(--gray-400); - color: var(--white); - } } diff --git a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.ts b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.ts index 39a712c64424..da22826776da 100644 --- a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.ts +++ b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.ts @@ -20,8 +20,6 @@ import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { getAsGroupChatDTO, isGroupChatDTO } from 'app/entities/metis/conversation/group-chat.model'; import { GroupChatService } from 'app/shared/metis/conversations/group-chat.service'; import { catchError } from 'rxjs/operators'; -import { getBackgroundColorHue } from 'app/utils/color.utils'; -import { getInitialsFromString } from 'app/utils/text.utils'; @Component({ // eslint-disable-next-line @angular-eslint/component-selector @@ -58,9 +56,9 @@ export class ConversationMemberRowComponent implements OnInit, OnDestroy { canBeRevokedChannelModeratorRole = false; userLabel: string; + userName: string | undefined; + userId: number | undefined; userImageUrl: string | undefined; - userDefaultPictureHue: string; - userInitials: string; // icons userIcon: IconProp = faUser; userTooltip = ''; @@ -94,9 +92,9 @@ export class ConversationMemberRowComponent implements OnInit, OnDestroy { } this.userImageUrl = this.conversationMember.imageUrl; + this.userId = this.conversationMember.id; + this.userName = this.conversationMember.name; this.userLabel = getUserLabel(this.conversationMember); - this.userInitials = getInitialsFromString(this.conversationMember.name ?? 'NA'); - this.userDefaultPictureHue = getBackgroundColorHue(this.conversationMember.id ? this.conversationMember.id.toString() : 'default'); this.setUserAuthorityIconAndTooltip(); // the creator of a channel can not be removed from the channel this.canBeRemovedFromConversation = !this.isCurrentUser && this.canRemoveUsersFromConversation(this.activeConversation); diff --git a/src/main/webapp/app/shared/course-group/course-group.component.html b/src/main/webapp/app/shared/course-group/course-group.component.html index a399e667e360..bdfe9046d82f 100644 --- a/src/main/webapp/app/shared/course-group/course-group.component.html +++ b/src/main/webapp/app/shared/course-group/course-group.component.html @@ -64,6 +64,25 @@

    } + + + + + + + + + + + diff --git a/src/main/webapp/app/shared/course-group/course-group.module.ts b/src/main/webapp/app/shared/course-group/course-group.module.ts index 6e25d333e3f5..0c145b7ad00c 100644 --- a/src/main/webapp/app/shared/course-group/course-group.module.ts +++ b/src/main/webapp/app/shared/course-group/course-group.module.ts @@ -5,9 +5,10 @@ import { UserImportModule } from 'app/shared/user-import/user-import.module'; import { NgxDatatableModule } from '@siemens/ngx-datatable'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { RouterModule } from '@angular/router'; +import { ProfilePictureComponent } from 'app/shared/profile-picture/profile-picture.component'; @NgModule({ - imports: [ArtemisDataTableModule, UserImportModule, NgxDatatableModule, ArtemisSharedModule, RouterModule], + imports: [ArtemisDataTableModule, UserImportModule, NgxDatatableModule, ArtemisSharedModule, RouterModule, ProfilePictureComponent], declarations: [CourseGroupComponent], exports: [CourseGroupComponent], }) diff --git a/src/main/webapp/app/shared/metis/metis.component.scss b/src/main/webapp/app/shared/metis/metis.component.scss index 50c9db5f3588..8b835a234d01 100644 --- a/src/main/webapp/app/shared/metis/metis.component.scss +++ b/src/main/webapp/app/shared/metis/metis.component.scss @@ -1,5 +1,3 @@ -$profile-picture-height: 2.15rem; - .post-result-information { font-size: small; font-style: italic; @@ -31,54 +29,10 @@ $profile-picture-height: 2.15rem; margin-left: 2.65rem; } -.post-profile-picture { - width: $profile-picture-height; - height: $profile-picture-height; - font-size: 0.9rem; - display: inline-flex; - align-items: center; - justify-content: center; - background-color: var(--gray-400); - color: var(--white); -} - -.post-profile-picture-wrap { - width: $profile-picture-height; - height: $profile-picture-height; - position: relative; -} - -.post-edit-profile-picture-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(#000, 0.6); - opacity: 0; - color: var(--white); -} - -.post-edit-profile-picture-overlay:hover { - opacity: 1; -} - .post-authority-icon-student { display: none; } -.post-profile-picture-student { - background-color: var(--cyan); -} - -.post-profile-picture-tutor { - background-color: var(--orange); -} - -.post-profile-picture-instructor { - background-color: var(--graph-red); -} - .reference { font-weight: 200; display: inline-flex; diff --git a/src/main/webapp/app/shared/metis/metis.module.ts b/src/main/webapp/app/shared/metis/metis.module.ts index 82214acaca79..f116f5b965d0 100644 --- a/src/main/webapp/app/shared/metis/metis.module.ts +++ b/src/main/webapp/app/shared/metis/metis.module.ts @@ -42,6 +42,7 @@ import { LinkPreviewModule } from 'app/shared/link-preview/link-preview.module'; import { LinkPreviewComponent } from 'app/shared/link-preview/components/link-preview/link-preview.component'; import { LinkPreviewContainerComponent } from 'app/shared/link-preview/components/link-preview-container/link-preview-container.component'; import { MetisConversationService } from 'app/shared/metis/metis-conversation.service'; +import { ProfilePictureComponent } from 'app/shared/profile-picture/profile-picture.component'; @NgModule({ imports: [ @@ -63,6 +64,7 @@ import { MetisConversationService } from 'app/shared/metis/metis-conversation.se MatFormFieldModule, MatDialogModule, LinkPreviewModule, + ProfilePictureComponent, ], declarations: [ PostingThreadComponent, diff --git a/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.html b/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.html index b869f78d9258..08c7c2a410c3 100644 --- a/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.html +++ b/src/main/webapp/app/shared/metis/posting-header/answer-post-header/answer-post-header.component.html @@ -2,35 +2,17 @@ -
    +
    @if (faqs?.length === 0) {

    } From e5def323d1fae5cdfea9c2425dddc3bcced358c1 Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:52:52 +0200 Subject: [PATCH 16/18] Development: Fix the broken git programming submission e2e tests (#9546) --- .../programming/service/localvc/LocalVCServletService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCServletService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCServletService.java index f4bcf1ea2e89..f5fa8b2c9243 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCServletService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/LocalVCServletService.java @@ -436,7 +436,11 @@ public void authorizeUser(String repositoryTypeOrUserName, User user, Programmin ProgrammingExerciseParticipation participation; try { - participation = programmingExerciseParticipationService.retrieveParticipationForRepository(repositoryTypeOrUserName, localVCRepositoryUri.toString()); + participation = programmingExerciseParticipationService.retrieveParticipationForRepository(exercise, repositoryTypeOrUserName, + localVCRepositoryUri.isPracticeRepository(), true); + + // TODO Add this back in when we have figured out what is incorrect in the playwright configuration for (MySQL, Local) + // participation = programmingExerciseParticipationService.retrieveParticipationForRepository(repositoryTypeOrUserName, localVCRepositoryUri.toString()); } catch (EntityNotFoundException e) { throw new LocalVCInternalException( From ed7a24f4aa54a9da297be1c3f6cf0205cca59cf1 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Thu, 24 Oct 2024 09:00:23 +0200 Subject: [PATCH 17/18] Development: Fix comment in FileResource --- src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java index e8ad0e1fc5fe..0582cc6d49d2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/FileResource.java @@ -483,7 +483,7 @@ public ResponseEntity getAttachmentUnitFile(@PathVariable Long courseId, } /** - * GET files/attachments/slides/attachment-unit/:attachmentUnitId/slide/:slideNumber : Get the lecture unit attachment slide by slide number + * GET files/attachments/attachment-unit/{attachmentUnitId}/slide/{slideNumber} : Get the lecture unit attachment slide by slide number * * @param attachmentUnitId ID of the attachment unit, the attachment belongs to * @param slideNumber the slideNumber of the file From caea9665dc6ed79559764114dc4682f50bc10b34 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Thu, 24 Oct 2024 10:12:51 +0200 Subject: [PATCH 18/18] Development: Bump version to 7.6.3 --- README.md | 2 +- build.gradle | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 89fa054c8626..ee2c87ae51ee 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ Refer to [Using JHipster in production](http://www.jhipster.tech/production) for The following command can automate the deployment to a server. The example shows the deployment to the main Artemis test server (which runs a virtual machine): ```shell -./artemis-server-cli deploy username@artemistest.ase.in.tum.de -w build/libs/Artemis-7.6.2.war +./artemis-server-cli deploy username@artemistest.ase.in.tum.de -w build/libs/Artemis-7.6.3.war ``` ## Architecture diff --git a/build.gradle b/build.gradle index 30502ab85ec7..4034de8043a4 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ plugins { } group = "de.tum.cit.aet.artemis" -version = "7.6.2" +version = "7.6.3" description = "Interactive Learning with Individual Feedback" java { diff --git a/package-lock.json b/package-lock.json index e932b0297d3b..4abc8ab32319 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "artemis", - "version": "7.6.2", + "version": "7.6.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "artemis", - "version": "7.6.2", + "version": "7.6.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 0128ec0da13e..fb1c4272fcbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "artemis", - "version": "7.6.2", + "version": "7.6.3", "description": "Interactive Learning with Individual Feedback", "private": true, "license": "MIT",