diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobsStatisticsDTO.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobsStatisticsDTO.java index db1aecdc0759..2537eec59d09 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobsStatisticsDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/BuildJobsStatisticsDTO.java @@ -4,10 +4,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; -import de.tum.cit.aet.artemis.programming.domain.build.BuildStatus; - @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record BuildJobsStatisticsDTO(long totalBuilds, long successfulBuilds, long failedBuilds, long cancelledBuilds) { +public record BuildJobsStatisticsDTO(long totalBuilds, long successfulBuilds, long failedBuilds, long cancelledBuilds, long timeOutBuilds, long missingBuilds) { /** * Create a BuildJobsStatisticsDTO from a list of BuildJobResultCountDTOs. @@ -20,19 +18,21 @@ public static BuildJobsStatisticsDTO of(List resultCount long successfulBuilds = 0; long failedBuilds = 0; long cancelledBuilds = 0; - // Switch case would cause an error in the testDTOImplementations test + long timeOutBuilds = 0; + long missingBuilds = 0; + long otherBuilds = 0; + for (BuildJobResultCountDTO resultCountDTO : resultCountDTOList) { - if (resultCountDTO.status() == BuildStatus.SUCCESSFUL) { - successfulBuilds += resultCountDTO.count(); - } - else if (resultCountDTO.status() == BuildStatus.FAILED || resultCountDTO.status() == BuildStatus.ERROR) { - failedBuilds += resultCountDTO.count(); - } - else if (resultCountDTO.status() == BuildStatus.CANCELLED) { - cancelledBuilds += resultCountDTO.count(); + switch (resultCountDTO.status()) { + case SUCCESSFUL -> successfulBuilds += resultCountDTO.count(); + case FAILED, ERROR -> failedBuilds += resultCountDTO.count(); + case CANCELLED -> cancelledBuilds += resultCountDTO.count(); + case TIMEOUT -> timeOutBuilds += resultCountDTO.count(); + case MISSING -> missingBuilds += resultCountDTO.count(); + default -> otherBuilds += resultCountDTO.count(); } } - totalBuilds = successfulBuilds + failedBuilds + cancelledBuilds; - return new BuildJobsStatisticsDTO(totalBuilds, successfulBuilds, failedBuilds, cancelledBuilds); + totalBuilds = successfulBuilds + failedBuilds + cancelledBuilds + timeOutBuilds + missingBuilds + otherBuilds; + return new BuildJobsStatisticsDTO(totalBuilds, successfulBuilds, failedBuilds, cancelledBuilds, timeOutBuilds, missingBuilds); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/FinishedBuildJobDTO.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/FinishedBuildJobDTO.java index d7df788928aa..a989f9b5c9b9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/FinishedBuildJobDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/dto/FinishedBuildJobDTO.java @@ -19,8 +19,8 @@ */ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record FinishedBuildJobDTO(String id, String name, String buildAgentAddress, long participationId, long courseId, long exerciseId, BuildStatus status, - RepositoryType repositoryType, String repositoryName, RepositoryType triggeredByPushTo, ZonedDateTime buildStartDate, ZonedDateTime buildCompletionDate, String commitHash, - ResultDTO submissionResult) { + RepositoryType repositoryType, String repositoryName, RepositoryType triggeredByPushTo, ZonedDateTime buildSubmissionDate, ZonedDateTime buildStartDate, + ZonedDateTime buildCompletionDate, String commitHash, ResultDTO submissionResult) { /** * A DTO representing a result @@ -65,6 +65,6 @@ public static FinishedBuildJobDTO of(BuildJob buildJob) { return new FinishedBuildJobDTO(buildJob.getBuildJobId(), buildJob.getName(), buildJob.getBuildAgentAddress(), buildJob.getParticipationId(), buildJob.getCourseId(), buildJob.getExerciseId(), buildJob.getBuildStatus(), buildJob.getRepositoryType(), buildJob.getRepositoryName(), buildJob.getTriggeredByPushTo(), - buildJob.getBuildStartDate(), buildJob.getBuildCompletionDate(), buildJob.getCommitHash(), resultDTO); + buildJob.getBuildSubmissionDate(), buildJob.getBuildStartDate(), buildJob.getBuildCompletionDate(), buildJob.getCommitHash(), resultDTO); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java index d8bed8b9f71e..a8641eeecccf 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobManagementService.java @@ -177,10 +177,17 @@ public CompletableFuture executeBuildJob(BuildJobQueueItem buildJob } else { finishBuildJobExceptionally(buildJobItem.id(), containerName, e); + + final String msg; if (e instanceof TimeoutException) { + msg = "Build job with id " + buildJobItem.id() + " was timed out"; logTimedOutBuildJob(buildJobItem, buildJobTimeoutSeconds); } - throw new CompletionException(e); + else { + msg = "Build job with id " + buildJobItem.id() + " failed"; + } + + throw new CompletionException(msg, e); } } }); 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 c833a52704ec..a0de8f0e4a15 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 @@ -449,12 +449,22 @@ private void processBuild(BuildJobQueueItem buildJob) { BuildJobQueueItem job; BuildStatus status; - if (!(ex.getCause() instanceof CancellationException) || !ex.getMessage().equals("Build job with id " + buildJob.id() + " was cancelled.")) { - status = BuildStatus.FAILED; - log.error("Error while processing build job: {}", buildJob, ex); + String cancelledMsg = "Build job with id " + buildJob.id() + " was cancelled."; + String timeoutMsg = "Build job with id " + buildJob.id() + " was timed out"; + Throwable cause = ex.getCause(); + String errorMessage = ex.getMessage(); + + if ((cause instanceof TimeoutException) || errorMessage.equals(timeoutMsg)) { + status = BuildStatus.TIMEOUT; + log.info("Build job with id {} was timed out", buildJob.id()); } - else { + else if ((cause instanceof CancellationException) && errorMessage.equals(cancelledMsg)) { status = BuildStatus.CANCELLED; + log.info("Build job with id {} was cancelled", buildJob.id()); + } + else { + status = BuildStatus.FAILED; + log.error("Error while processing build job: {}", buildJob, ex); } job = new BuildJobQueueItem(buildJob, completionDate, status); 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 7a6aeafdbd04..1a534e94835f 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 @@ -45,6 +45,9 @@ public class BuildJob extends DomainObject { @Column(name = "build_agent_address") private String buildAgentAddress; + @Column(name = "build_submission_date") + private ZonedDateTime buildSubmissionDate; + @Column(name = "build_start_date") private ZonedDateTime buildStartDate; @@ -89,6 +92,7 @@ public BuildJob(BuildJobQueueItem queueItem, BuildStatus buildStatus, Result res this.participationId = queueItem.participationId(); this.result = result; this.buildAgentAddress = queueItem.buildAgent().memberAddress(); + this.buildSubmissionDate = queueItem.jobTimingInfo().submissionDate(); this.buildStartDate = queueItem.jobTimingInfo().buildStartDate(); this.buildCompletionDate = queueItem.jobTimingInfo().buildCompletionDate(); this.repositoryType = queueItem.repositoryInfo().repositoryType(); @@ -157,6 +161,14 @@ public void setBuildAgentAddress(String buildAgentAddress) { this.buildAgentAddress = buildAgentAddress; } + public ZonedDateTime getBuildSubmissionDate() { + return buildSubmissionDate; + } + + public void setBuildSubmissionDate(ZonedDateTime buildSubmissionDate) { + this.buildSubmissionDate = buildSubmissionDate; + } + public ZonedDateTime getBuildStartDate() { return buildStartDate; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/build/BuildStatus.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/build/BuildStatus.java index 0b03f877c4f9..81d5ccb79059 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/build/BuildStatus.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/build/BuildStatus.java @@ -5,8 +5,11 @@ * FAILED: the build failed * ERROR: the build produced an error * CANCELED: the build was canceled + * QUEUED: the build is queued + * BUILDING: the build is currently building + * MISSING: the build is missing (i.e. it was not found in the queue, not being built or not finished) */ public enum BuildStatus { - SUCCESSFUL, FAILED, ERROR, CANCELLED + SUCCESSFUL, FAILED, ERROR, CANCELLED, QUEUED, BUILDING, TIMEOUT, MISSING } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildJobRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildJobRepository.java index 834a7dfa621e..9d195e9ec297 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildJobRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/BuildJobRepository.java @@ -14,9 +14,11 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import de.tum.cit.aet.artemis.buildagent.dto.BuildJobResultCountDTO; import de.tum.cit.aet.artemis.buildagent.dto.DockerImageBuild; @@ -39,8 +41,8 @@ public interface BuildJobRepository extends ArtemisJpaRepository LEFT JOIN Course c ON b.courseId = c.id WHERE (:buildStatus IS NULL OR b.buildStatus = :buildStatus) AND (:buildAgentAddress IS NULL OR b.buildAgentAddress = :buildAgentAddress) - AND (CAST(:startDate AS string) IS NULL OR b.buildStartDate >= :startDate) - AND (CAST(:endDate AS string) IS NULL OR b.buildStartDate <= :endDate) + AND (CAST(:startDate AS string) IS NULL OR b.buildSubmissionDate >= :startDate) + AND (CAST(:endDate AS string) IS NULL OR b.buildSubmissionDate <= :endDate) AND (:searchTerm IS NULL OR (b.repositoryName LIKE %:searchTerm% OR c.title LIKE %:searchTerm%)) AND (:courseId IS NULL OR b.courseId = :courseId) AND (:durationLower IS NULL OR (b.buildCompletionDate - b.buildStartDate) >= :durationLower) @@ -77,7 +79,7 @@ Page findIdsByFilterCriteria(@Param("buildStatus") BuildStatus buildStatus COUNT(b.buildStatus) ) FROM BuildJob b - WHERE b.buildStartDate >= :fromDateTime + WHERE b.buildSubmissionDate >= :fromDateTime AND (:courseId IS NULL OR b.courseId = :courseId) GROUP BY b.buildStatus """) @@ -120,4 +122,37 @@ SELECT COUNT(b) WHERE b.exerciseId = :exerciseId AND b.buildStatus = 'SUCCESSFUL' """) long fetchSuccessfulBuildJobCountByExerciseId(@Param("exerciseId") Long exerciseId); + + @Transactional + @Modifying + @Query("UPDATE BuildJob b SET b.buildStatus = :newStatus WHERE b.buildJobId = :buildJobId") + void updateBuildJobStatus(@Param("buildJobId") String buildJobId, @Param("newStatus") BuildStatus newStatus); + + /** + * Update the build job status and set the build start date if it is not set yet. The buildStartDate is required to calculate the statistics and the correctly display in the + * build overview. + * This is used to update missing jobs that do not have a build start date yet. + * + * @param buildJobId the build job id + * @param newStatus the new build status + * @param buildStartDate the build start date + */ + @Transactional + @Modifying + @Query(""" + UPDATE BuildJob b + SET b.buildStatus = :newStatus, + b.buildStartDate = CASE WHEN b.buildStartDate IS NULL THEN :buildStartDate ELSE b.buildStartDate END + WHERE b.buildJobId = :buildJobId + """) + void updateBuildJobStatusWithBuildStartDate(@Param("buildJobId") String buildJobId, @Param("newStatus") BuildStatus newStatus, + @Param("buildStartDate") ZonedDateTime buildStartDate); + + /** + * Find all build jobs with the given build status. + * + * @param statuses the list of build statuses + * @return the list of build jobs + */ + List findAllByBuildStatusIn(List statuses); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIEventListenerService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIEventListenerService.java new file mode 100644 index 000000000000..b2d75b052531 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIEventListenerService.java @@ -0,0 +1,182 @@ +package de.tum.cit.aet.artemis.programming.service.localci; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import jakarta.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import com.hazelcast.collection.IQueue; +import com.hazelcast.collection.ItemEvent; +import com.hazelcast.collection.ItemListener; +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.map.IMap; +import com.hazelcast.map.listener.EntryAddedListener; +import com.hazelcast.map.listener.EntryRemovedListener; +import com.hazelcast.map.listener.EntryUpdatedListener; + +import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentInformation; +import de.tum.cit.aet.artemis.buildagent.dto.BuildJobQueueItem; +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.dto.SubmissionProcessingDTO; +import de.tum.cit.aet.artemis.programming.repository.BuildJobRepository; +import de.tum.cit.aet.artemis.programming.service.ProgrammingMessagingService; + +@Service +@Profile("localci & scheduling") +public class LocalCIEventListenerService { + + private static final Logger log = LoggerFactory.getLogger(LocalCIEventListenerService.class); + + private final HazelcastInstance hazelcastInstance; + + private final LocalCIQueueWebsocketService localCIQueueWebsocketService; + + private final BuildJobRepository buildJobRepository; + + private final SharedQueueManagementService sharedQueueManagementService; + + private final ProgrammingMessagingService programmingMessagingService; + + public LocalCIEventListenerService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, LocalCIQueueWebsocketService localCIQueueWebsocketService, + BuildJobRepository buildJobRepository, SharedQueueManagementService sharedQueueManagementService, ProgrammingMessagingService programmingMessagingService) { + this.hazelcastInstance = hazelcastInstance; + this.localCIQueueWebsocketService = localCIQueueWebsocketService; + this.buildJobRepository = buildJobRepository; + this.sharedQueueManagementService = sharedQueueManagementService; + this.programmingMessagingService = programmingMessagingService; + } + + /** + * Add listeners for build job, build agent changes. + */ + @PostConstruct + public void init() { + IQueue queue = hazelcastInstance.getQueue("buildJobQueue"); + IMap processingJobs = hazelcastInstance.getMap("processingJobs"); + IMap buildAgentInformation = hazelcastInstance.getMap("buildAgentInformation"); + queue.addItemListener(new QueuedBuildJobItemListener(), true); + processingJobs.addEntryListener(new ProcessingBuildJobItemListener(), true); + buildAgentInformation.addEntryListener(new BuildAgentListener(), true); + } + + /** + * Check the status of pending build jobs. If a build job is missing from the queue, not being built or not finished, update the status to missing. + * Default interval is 5 minutes. Default delay is 1 minute. + */ + @Scheduled(fixedRateString = "${artemis.continuous-integration.check-job-status-interval-seconds:300}", initialDelayString = "${artemis.continuous-integration.check-job-status-delay-seconds:60}", timeUnit = TimeUnit.SECONDS) + public void checkPendingBuildJobsStatus() { + log.info("Checking pending build jobs status"); + List pendingBuildJobs = buildJobRepository.findAllByBuildStatusIn(List.of(BuildStatus.QUEUED, BuildStatus.BUILDING)); + ZonedDateTime now = ZonedDateTime.now(); + final int buildJobExpirationInMinutes = 5; // If a build job is older than 5 minutes, and it's status can't be determined, set it to missing + for (BuildJob buildJob : pendingBuildJobs) { + if (buildJob.getBuildSubmissionDate().isAfter(now.minusMinutes(buildJobExpirationInMinutes))) { + log.debug("Build job with id {} is too recent to check", buildJob.getBuildJobId()); + continue; + } + + if (buildJob.getBuildStatus() == BuildStatus.QUEUED && checkIfBuildJobIsStillQueued(buildJob.getBuildJobId())) { + log.debug("Build job with id {} is still queued", buildJob.getBuildJobId()); + continue; + } + if (checkIfBuildJobIsStillBuilding(buildJob.getBuildJobId())) { + log.debug("Build job with id {} is still building", buildJob.getBuildJobId()); + continue; + } + if (checkIfBuildJobHasFinished(buildJob.getBuildJobId())) { + log.debug("Build job with id {} has finished", buildJob.getBuildJobId()); + continue; + } + log.error("Build job with id {} is in an unknown state", buildJob.getBuildJobId()); + // If the build job is in an unknown state, set it to missing and update the build start date + buildJobRepository.updateBuildJobStatus(buildJob.getBuildJobId(), BuildStatus.MISSING); + } + } + + private boolean checkIfBuildJobIsStillBuilding(String buildJobId) { + return sharedQueueManagementService.getProcessingJobIds().contains(buildJobId); + } + + private boolean checkIfBuildJobIsStillQueued(String buildJobId) { + return sharedQueueManagementService.getQueuedJobs().stream().anyMatch(job -> job.id().equals(buildJobId)); + } + + private boolean checkIfBuildJobHasFinished(String buildJobId) { + var buildJobOpt = buildJobRepository.findByBuildJobId(buildJobId); + if (buildJobOpt.isEmpty()) { + log.error("Build job with id {} not found in database", buildJobId); + return false; + } + var buildJob = buildJobOpt.get(); + return buildJob.getBuildStatus() != BuildStatus.QUEUED && buildJob.getBuildStatus() != BuildStatus.BUILDING; + } + + private class QueuedBuildJobItemListener implements ItemListener { + + @Override + public void itemAdded(ItemEvent event) { + localCIQueueWebsocketService.sendQueuedJobsOverWebsocket(event.getItem().courseId()); + } + + @Override + public void itemRemoved(ItemEvent event) { + localCIQueueWebsocketService.sendQueuedJobsOverWebsocket(event.getItem().courseId()); + } + } + + private class ProcessingBuildJobItemListener implements EntryAddedListener, EntryRemovedListener { + + @Override + public void entryAdded(com.hazelcast.core.EntryEvent event) { + log.debug("CIBuildJobQueueItem added to processing jobs: {}", event.getValue()); + localCIQueueWebsocketService.sendProcessingJobsOverWebsocket(event.getValue().courseId()); + buildJobRepository.updateBuildJobStatusWithBuildStartDate(event.getValue().id(), BuildStatus.BUILDING, event.getValue().jobTimingInfo().buildStartDate()); + notifyUserAboutBuildProcessing(event.getValue().exerciseId(), event.getValue().participationId(), event.getValue().buildConfig().assignmentCommitHash(), + event.getValue().jobTimingInfo().submissionDate(), event.getValue().jobTimingInfo().buildStartDate(), + event.getValue().jobTimingInfo().estimatedCompletionDate()); + } + + @Override + public void entryRemoved(com.hazelcast.core.EntryEvent event) { + log.debug("CIBuildJobQueueItem removed from processing jobs: {}", event.getOldValue()); + localCIQueueWebsocketService.sendProcessingJobsOverWebsocket(event.getOldValue().courseId()); + } + } + + private class BuildAgentListener + implements EntryAddedListener, EntryRemovedListener, EntryUpdatedListener { + + @Override + public void entryAdded(com.hazelcast.core.EntryEvent event) { + log.debug("Build agent added: {}", event.getValue()); + localCIQueueWebsocketService.sendBuildAgentInformationOverWebsocket(event.getValue().buildAgent().name()); + } + + @Override + public void entryRemoved(com.hazelcast.core.EntryEvent event) { + log.debug("Build agent removed: {}", event.getOldValue()); + localCIQueueWebsocketService.sendBuildAgentInformationOverWebsocket(event.getOldValue().buildAgent().name()); + } + + @Override + public void entryUpdated(com.hazelcast.core.EntryEvent event) { + log.debug("Build agent updated: {}", event.getValue()); + localCIQueueWebsocketService.sendBuildAgentInformationOverWebsocket(event.getValue().buildAgent().name()); + } + } + + private void notifyUserAboutBuildProcessing(long exerciseId, long participationId, String commitHash, ZonedDateTime submissionDate, ZonedDateTime buildStartDate, + ZonedDateTime estimatedCompletionDate) { + var submissionProcessingDTO = new SubmissionProcessingDTO(exerciseId, participationId, commitHash, submissionDate, buildStartDate, estimatedCompletionDate); + programmingMessagingService.notifyUserAboutSubmissionProcessing(submissionProcessingDTO, exerciseId, participationId); + } +} 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 f82777abddb2..60ef581872c2 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 @@ -1,32 +1,15 @@ package de.tum.cit.aet.artemis.programming.service.localci; -import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; -import jakarta.annotation.PostConstruct; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import com.hazelcast.collection.IQueue; -import com.hazelcast.collection.ItemEvent; -import com.hazelcast.collection.ItemListener; -import com.hazelcast.core.HazelcastInstance; -import com.hazelcast.map.IMap; -import com.hazelcast.map.listener.EntryAddedListener; -import com.hazelcast.map.listener.EntryRemovedListener; -import com.hazelcast.map.listener.EntryUpdatedListener; - 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; import de.tum.cit.aet.artemis.buildagent.dto.RepositoryInfo; -import de.tum.cit.aet.artemis.programming.dto.SubmissionProcessingDTO; -import de.tum.cit.aet.artemis.programming.service.ProgrammingMessagingService; /** * This service is responsible for sending build job queue information over websockets. @@ -38,58 +21,55 @@ @Profile("localci & scheduling") public class LocalCIQueueWebsocketService { - private static final Logger log = LoggerFactory.getLogger(LocalCIQueueWebsocketService.class); - private final LocalCIWebsocketMessagingService localCIWebsocketMessagingService; - private final ProgrammingMessagingService programmingMessagingService; - private final SharedQueueManagementService sharedQueueManagementService; - private final HazelcastInstance hazelcastInstance; - /** * Instantiates a new Local ci queue websocket service. * - * @param hazelcastInstance the hazelcast instance * @param localCIWebsocketMessagingService the local ci build queue websocket service * @param sharedQueueManagementService the local ci shared build job queue service */ - public LocalCIQueueWebsocketService(@Qualifier("hazelcastInstance") HazelcastInstance hazelcastInstance, LocalCIWebsocketMessagingService localCIWebsocketMessagingService, - SharedQueueManagementService sharedQueueManagementService, ProgrammingMessagingService programmingMessagingService) { - this.hazelcastInstance = hazelcastInstance; + public LocalCIQueueWebsocketService(LocalCIWebsocketMessagingService localCIWebsocketMessagingService, SharedQueueManagementService sharedQueueManagementService) { this.localCIWebsocketMessagingService = localCIWebsocketMessagingService; this.sharedQueueManagementService = sharedQueueManagementService; - this.programmingMessagingService = programmingMessagingService; } /** - * Add listeners for build job queue changes. + * Sends queued jobs over websocket. This method is called when a new job is added to the queue or a job is removed from the queue. + * + * @param courseId the course id of the programming exercise related to the job */ - @PostConstruct - public void init() { - IQueue queue = hazelcastInstance.getQueue("buildJobQueue"); - IMap processingJobs = hazelcastInstance.getMap("processingJobs"); - IMap buildAgentInformation = hazelcastInstance.getMap("buildAgentInformation"); - queue.addItemListener(new QueuedBuildJobItemListener(), true); - processingJobs.addEntryListener(new ProcessingBuildJobItemListener(), true); - buildAgentInformation.addEntryListener(new BuildAgentListener(), true); - } - - private void sendQueuedJobsOverWebsocket(long courseId) { + void sendQueuedJobsOverWebsocket(long courseId) { var queuedJobs = removeUnnecessaryInformation(sharedQueueManagementService.getQueuedJobs()); var queuedJobsForCourse = queuedJobs.stream().filter(job -> job.courseId() == courseId).toList(); localCIWebsocketMessagingService.sendQueuedBuildJobs(queuedJobs); localCIWebsocketMessagingService.sendQueuedBuildJobsForCourse(courseId, queuedJobsForCourse); } - private void sendProcessingJobsOverWebsocket(long courseId) { + /** + * Sends processing jobs over websocket. This method is called when a new job is added to the processing jobs or a job is removed from the processing jobs. + * + * @param courseId the course id of the programming exercise related to the job + */ + void sendProcessingJobsOverWebsocket(long courseId) { var processingJobs = removeUnnecessaryInformation(sharedQueueManagementService.getProcessingJobs()); var processingJobsForCourse = processingJobs.stream().filter(job -> job.courseId() == courseId).toList(); localCIWebsocketMessagingService.sendRunningBuildJobs(processingJobs); localCIWebsocketMessagingService.sendRunningBuildJobsForCourse(courseId, processingJobsForCourse); } + /** + * Sends build agent information over websocket. This method is called when a new build agent is added or removed. + * + * @param agentName the name of the build agent + */ + void sendBuildAgentInformationOverWebsocket(String agentName) { + sendBuildAgentSummaryOverWebsocket(); + sendBuildAgentDetailsOverWebsocket(agentName); + } + private void sendBuildAgentSummaryOverWebsocket() { var buildAgentSummary = removeUnnecessaryInformationFromBuildAgentInformation(sharedQueueManagementService.getBuildAgentInformationWithoutRecentBuildJobs()); localCIWebsocketMessagingService.sendBuildAgentSummary(buildAgentSummary); @@ -100,64 +80,6 @@ private void sendBuildAgentDetailsOverWebsocket(String agentName) { .ifPresent(localCIWebsocketMessagingService::sendBuildAgentDetails); } - private void sendBuildAgentInformationOverWebsocket(String agentName) { - sendBuildAgentSummaryOverWebsocket(); - sendBuildAgentDetailsOverWebsocket(agentName); - } - - private class QueuedBuildJobItemListener implements ItemListener { - - @Override - public void itemAdded(ItemEvent event) { - sendQueuedJobsOverWebsocket(event.getItem().courseId()); - } - - @Override - public void itemRemoved(ItemEvent event) { - sendQueuedJobsOverWebsocket(event.getItem().courseId()); - } - } - - private class ProcessingBuildJobItemListener implements EntryAddedListener, EntryRemovedListener { - - @Override - public void entryAdded(com.hazelcast.core.EntryEvent event) { - log.debug("CIBuildJobQueueItem added to processing jobs: {}", event.getValue()); - sendProcessingJobsOverWebsocket(event.getValue().courseId()); - notifyUserAboutBuildProcessing(event.getValue().exerciseId(), event.getValue().participationId(), event.getValue().buildConfig().assignmentCommitHash(), - event.getValue().jobTimingInfo().submissionDate(), event.getValue().jobTimingInfo().buildStartDate(), - event.getValue().jobTimingInfo().estimatedCompletionDate()); - } - - @Override - public void entryRemoved(com.hazelcast.core.EntryEvent event) { - log.debug("CIBuildJobQueueItem removed from processing jobs: {}", event.getOldValue()); - sendProcessingJobsOverWebsocket(event.getOldValue().courseId()); - } - } - - private class BuildAgentListener - implements EntryAddedListener, EntryRemovedListener, EntryUpdatedListener { - - @Override - public void entryAdded(com.hazelcast.core.EntryEvent event) { - log.debug("Build agent added: {}", event.getValue()); - sendBuildAgentInformationOverWebsocket(event.getValue().buildAgent().name()); - } - - @Override - public void entryRemoved(com.hazelcast.core.EntryEvent event) { - log.debug("Build agent removed: {}", event.getOldValue()); - sendBuildAgentInformationOverWebsocket(event.getOldValue().buildAgent().name()); - } - - @Override - public void entryUpdated(com.hazelcast.core.EntryEvent event) { - log.debug("Build agent updated: {}", event.getValue()); - sendBuildAgentInformationOverWebsocket(event.getValue().buildAgent().name()); - } - } - /** * Removes unnecessary information (e.g. repository info, build config, result) from the queued jobs before sending them over the websocket. * @@ -211,9 +133,4 @@ private static List removeUnnecessaryInformationFromBuild return filteredBuildAgentSummary; } - private void notifyUserAboutBuildProcessing(long exerciseId, long participationId, String commitHash, ZonedDateTime submissionDate, ZonedDateTime buildStartDate, - ZonedDateTime estimatedCompletionDate) { - var submissionProcessingDTO = new SubmissionProcessingDTO(exerciseId, participationId, commitHash, submissionDate, buildStartDate, estimatedCompletionDate); - programmingMessagingService.notifyUserAboutSubmissionProcessing(submissionProcessingDTO, exerciseId, participationId); - } } 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 a453c7accf5b..7acc57856c37 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 @@ -9,6 +9,7 @@ import java.util.UUID; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; import jakarta.annotation.PreDestroy; @@ -185,6 +186,9 @@ public void processResult() { if (ex.getCause() instanceof CancellationException && ex.getMessage().equals("Build job with id " + buildJob.id() + " was cancelled.")) { savedBuildJob = saveFinishedBuildJob(buildJob, BuildStatus.CANCELLED, result); } + else if (ex.getCause() instanceof TimeoutException && ex.getMessage().equals("Build job with id " + buildJob.id() + " was timed out")) { + savedBuildJob = saveFinishedBuildJob(buildJob, BuildStatus.TIMEOUT, result); + } else { log.error("Error while processing build job: {}", buildJob, ex); savedBuildJob = saveFinishedBuildJob(buildJob, BuildStatus.FAILED, result); @@ -275,6 +279,9 @@ private void addResultToBuildAgentsRecentBuildJobs(BuildJobQueueItem buildJob, R private BuildJob saveFinishedBuildJob(BuildJobQueueItem queueItem, BuildStatus buildStatus, Result result) { try { BuildJob buildJob = new BuildJob(queueItem, buildStatus, result); + buildJobRepository.findByBuildJobId(queueItem.id()).ifPresent(existingBuildJob -> { + buildJob.setId(existingBuildJob.getId()); + }); return buildJobRepository.save(buildJob); } catch (Exception e) { 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 0ae4407e885d..4f56ca6cacd1 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 @@ -41,8 +41,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.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.dto.aeolus.Windfile; import de.tum.cit.aet.artemis.programming.repository.AuxiliaryRepositoryRepository; +import de.tum.cit.aet.artemis.programming.repository.BuildJobRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildStatisticsRepository; import de.tum.cit.aet.artemis.programming.repository.SolutionProgrammingExerciseParticipationRepository; @@ -107,6 +110,8 @@ public class LocalCITriggerService implements ContinuousIntegrationTriggerServic private final ProgrammingExerciseBuildConfigService programmingExerciseBuildConfigService; + private final BuildJobRepository buildJobRepository; + private static final int DEFAULT_BUILD_DURATION = 17; // Arbitrary value to ensure that the build duration is always a bit higher than the actual build duration @@ -118,7 +123,7 @@ public LocalCITriggerService(@Qualifier("hazelcastInstance") HazelcastInstance h SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, LocalCIBuildConfigurationService localCIBuildConfigurationService, GitService gitService, ExerciseDateService exerciseDateService, ProgrammingExerciseBuildConfigRepository programmingExerciseBuildConfigRepository, BuildScriptProviderService buildScriptProviderService, - ProgrammingExerciseBuildConfigService programmingExerciseBuildConfigService, + ProgrammingExerciseBuildConfigService programmingExerciseBuildConfigService, BuildJobRepository buildJobRepository, ProgrammingExerciseBuildStatisticsRepository programmingExerciseBuildStatisticsRepository) { this.hazelcastInstance = hazelcastInstance; this.aeolusTemplateService = aeolusTemplateService; @@ -134,6 +139,7 @@ public LocalCITriggerService(@Qualifier("hazelcastInstance") HazelcastInstance h this.buildScriptProviderService = buildScriptProviderService; this.programmingExerciseBuildConfigService = programmingExerciseBuildConfigService; this.programmingExerciseBuildStatisticsRepository = programmingExerciseBuildStatisticsRepository; + this.buildJobRepository = buildJobRepository; } @PostConstruct @@ -222,6 +228,8 @@ else if (triggeredByPushTo.equals(RepositoryType.TESTS)) { BuildJobQueueItem buildJobQueueItem = new BuildJobQueueItem(buildJobId, participation.getBuildPlanId(), buildAgent, participation.getId(), courseId, programmingExercise.getId(), 0, priority, null, repositoryInfo, jobTimingInfo, buildConfig, null); + // Save the build job before adding it to the queue in case it got quickly processed. Update statement would then fail. + buildJobRepository.save(new BuildJob(buildJobQueueItem, BuildStatus.QUEUED, null)); queue.add(buildJobQueueItem); log.info("Added build job {} for exercise {} and participation {} with priority {} to the queue", buildJobId, programmingExercise.getShortName(), participation.getId(), priority); 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 1ed01bf6e7ac..f66ada18e280 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 @@ -42,6 +42,7 @@ import de.tum.cit.aet.artemis.core.dto.pageablesearch.FinishedBuildJobPageableSearchDTO; import de.tum.cit.aet.artemis.core.service.ProfileService; 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.repository.BuildJobRepository; /** @@ -139,6 +140,14 @@ public List getProcessingJobs() { return new ArrayList<>(processingJobs.values()); } + /** + * @return a list of processing job ids + */ + public List getProcessingJobIds() { + // NOTE: we should not use streams with IMap, because it can be unstable, when many items are added at the same time and there is a slow network condition + return new ArrayList<>(processingJobs.keySet()); + } + public int getProcessingJobsSize() { return processingJobs.size(); } @@ -205,6 +214,7 @@ public void cancelBuildJob(String buildJobId) { } } queue.removeAll(toRemove); + updateCancelledQueuedBuildJobsStatus(toRemove); } else { // Cancel build job if it is currently being processed @@ -215,6 +225,12 @@ public void cancelBuildJob(String buildJobId) { } } + private void updateCancelledQueuedBuildJobsStatus(List queuedJobs) { + for (BuildJobQueueItem queuedJob : queuedJobs) { + buildJobRepository.updateBuildJobStatus(queuedJob.id(), BuildStatus.CANCELLED); + } + } + /** * Trigger the cancellation of the build job for the given buildJobId. * The listener for the canceledBuildJobsTopic will then cancel the build job. @@ -231,7 +247,9 @@ private void triggerBuildJobCancellation(String buildJobId) { */ public void cancelAllQueuedBuildJobs() { log.debug("Cancelling all queued build jobs"); + List queuedJobs = getQueuedJobs(); queue.clear(); + updateCancelledQueuedBuildJobsStatus(queuedJobs); } /** @@ -267,6 +285,7 @@ public void cancelAllQueuedBuildJobsForCourse(long courseId) { } } queue.removeAll(toRemove); + updateCancelledQueuedBuildJobsStatus(toRemove); } /** @@ -297,6 +316,7 @@ public void cancelAllJobsForParticipation(long participationId) { } } queue.removeAll(toRemove); + updateCancelledQueuedBuildJobsStatus(toRemove); List runningJobs = getProcessingJobs(); for (BuildJobQueueItem runningJob : runningJobs) { diff --git a/src/main/resources/config/liquibase/changelog/20241213200000_changelog.xml b/src/main/resources/config/liquibase/changelog/20241213200000_changelog.xml new file mode 100644 index 000000000000..54bb6de1727f --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241213200000_changelog.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + UPDATE build_job + SET build_submission_date = build_start_date + WHERE build_submission_date IS NULL; + + + \ 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 02ac0209b9d1..66213900352f 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -42,6 +42,7 @@ + diff --git a/src/main/webapp/app/admin/admin.module.ts b/src/main/webapp/app/admin/admin.module.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/main/webapp/app/course/manage/course-management.module.ts b/src/main/webapp/app/course/manage/course-management.module.ts index bf0afc89a1d2..d2e6eaf9024d 100644 --- a/src/main/webapp/app/course/manage/course-management.module.ts +++ b/src/main/webapp/app/course/manage/course-management.module.ts @@ -49,11 +49,9 @@ import { CourseManagementExercisesSearchComponent } from 'app/course/manage/cour import { LineChartModule, PieChartModule } from '@swimlane/ngx-charts'; import { ArtemisPlagiarismModule } from 'app/exercises/shared/plagiarism/plagiarism.module'; import { ArtemisChartsModule } from 'app/shared/chart/artemis-charts.module'; - import { ArtemisFullscreenModule } from 'app/shared/fullscreen/fullscreen.module'; import { ArtemisCourseGroupModule } from 'app/shared/course-group/course-group.module'; import { CourseGroupMembershipComponent } from './course-group-membership/course-group-membership.component'; - import { CourseLtiConfigurationComponent } from 'app/course/manage/course-lti-configuration/course-lti-configuration.component'; import { EditCourseLtiConfigurationComponent } from 'app/course/manage/course-lti-configuration/edit-course-lti-configuration.component'; import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; 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 2c88534d2626..481ece04df79 100644 --- a/src/main/webapp/app/entities/programming/build-job.model.ts +++ b/src/main/webapp/app/entities/programming/build-job.model.ts @@ -35,6 +35,7 @@ export class FinishedBuildJob implements StringBaseEntity { public triggeredByPushTo?: TriggeredByPushTo; public repositoryName?: string; public repositoryType?: string; + public buildSubmissionDate?: dayjs.Dayjs; public buildStartDate?: dayjs.Dayjs; public buildCompletionDate?: dayjs.Dayjs; public buildDuration?: string; @@ -47,6 +48,8 @@ export class BuildJobStatistics { public successfulBuilds: number = 0; public failedBuilds: number = 0; public cancelledBuilds: number = 0; + public timeOutBuilds: number = 0; + public missingBuilds: number = 0; } export enum SpanType { diff --git a/src/main/webapp/app/localci/build-queue/build-job-statistics/build-job-statistics.component.html b/src/main/webapp/app/localci/build-queue/build-job-statistics/build-job-statistics.component.html new file mode 100644 index 000000000000..8f466e0424e6 --- /dev/null +++ b/src/main/webapp/app/localci/build-queue/build-job-statistics/build-job-statistics.component.html @@ -0,0 +1,90 @@ +
+
+
+

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

{{ successfulBuildsPercentage }}

+

+
+
+
+
+ + {{ buildJobStatistics.totalBuilds }} +
+
+ + {{ buildJobStatistics.successfulBuilds }} +
+
+
+
+ + {{ failedBuildsPercentage }} +
+
+ + {{ buildJobStatistics.failedBuilds }} +
+
+
+
+ + {{ cancelledBuildsPercentage }} +
+
+ + {{ buildJobStatistics.cancelledBuilds }} +
+
+
+
+ + {{ timeoutBuildsPercentage }} +
+
+ + {{ buildJobStatistics.timeOutBuilds }} +
+
+
+
+ + {{ missingBuildsPercentage }} +
+
+ + {{ buildJobStatistics.missingBuilds }} +
+
+
+
+ +
+
+
+
diff --git a/src/main/webapp/app/localci/build-queue/build-job-statistics/build-job-statistics.component.scss b/src/main/webapp/app/localci/build-queue/build-job-statistics/build-job-statistics.component.scss new file mode 100644 index 000000000000..ccc4c66a06ad --- /dev/null +++ b/src/main/webapp/app/localci/build-queue/build-job-statistics/build-job-statistics.component.scss @@ -0,0 +1,8 @@ +.result-card { + width: 8vw; + min-width: 115px; +} + +.stats-column { + min-width: 230px; +} diff --git a/src/main/webapp/app/localci/build-queue/build-job-statistics/build-job-statistics.component.ts b/src/main/webapp/app/localci/build-queue/build-job-statistics/build-job-statistics.component.ts new file mode 100644 index 000000000000..256627802a42 --- /dev/null +++ b/src/main/webapp/app/localci/build-queue/build-job-statistics/build-job-statistics.component.ts @@ -0,0 +1,125 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { BuildJobStatistics, SpanType } from 'app/entities/programming/build-job.model'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { faAngleDown, faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { onError } from 'app/shared/util/global.utils'; +import { NgxChartsSingleSeriesDataEntry } from 'app/shared/chart/ngx-charts-datatypes'; +import { GraphColors } from 'app/entities/statistics.model'; +import { Color, NgxChartsModule, ScaleType } from '@swimlane/ngx-charts'; +import { BuildQueueService } from 'app/localci/build-queue/build-queue.service'; +import { ActivatedRoute } from '@angular/router'; +import { AlertService } from 'app/core/util/alert.service'; +import { take } from 'rxjs/operators'; +import { HttpErrorResponse } from '@angular/common/http'; +import { ArtemisChartsModule } from 'app/shared/chart/artemis-charts.module'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'jhi-build-job-statistics', + standalone: true, + imports: [ArtemisSharedComponentModule, TranslateDirective, ArtemisChartsModule, ArtemisSharedModule, NgxChartsModule, NgbCollapse], + templateUrl: './build-job-statistics.component.html', + styleUrl: './build-job-statistics.component.scss', +}) +export class BuildJobStatisticsComponent implements OnInit { + private buildQueueService = inject(BuildQueueService); + private route = inject(ActivatedRoute); + private alertService = inject(AlertService); + + readonly faAngleDown = faAngleDown; + readonly faAngleRight = faAngleRight; + + protected readonly SpanType = SpanType; + currentSpan: SpanType = SpanType.WEEK; + + isCollapsed = false; + successfulBuildsPercentage: string; + failedBuildsPercentage: string; + cancelledBuildsPercentage: string; + timeoutBuildsPercentage: string; + missingBuildsPercentage: string; + + buildJobStatistics = new BuildJobStatistics(); + + ngOnInit() { + this.getBuildJobStatistics(this.currentSpan); + } + + ngxData: NgxChartsSingleSeriesDataEntry[] = []; + + ngxColor = { + name: 'vivid', + selectable: true, + group: ScaleType.Ordinal, + domain: [GraphColors.GREEN, GraphColors.RED, GraphColors.YELLOW, GraphColors.BLUE, GraphColors.GREY], + } as Color; + + /** + * Get Build Job Result statistics. Should be called in admin view only. + */ + getBuildJobStatistics(span: SpanType = SpanType.WEEK) { + this.route.paramMap.pipe(take(1)).subscribe((params) => { + const courseId = Number(params.get('courseId')); + if (courseId) { + this.buildQueueService.getBuildJobStatisticsForCourse(courseId, span).subscribe({ + next: (res: BuildJobStatistics) => { + this.updateDisplayedBuildJobStatistics(res); + }, + error: (res: HttpErrorResponse) => { + onError(this.alertService, res); + }, + }); + } else { + this.buildQueueService.getBuildJobStatistics(span).subscribe({ + next: (res: BuildJobStatistics) => { + this.updateDisplayedBuildJobStatistics(res); + }, + error: (res: HttpErrorResponse) => { + onError(this.alertService, res); + }, + }); + } + }); + } + + /** + * Update the displayed build job statistics + * @param stats The new build job statistics + */ + updateDisplayedBuildJobStatistics(stats: BuildJobStatistics) { + this.buildJobStatistics = stats; + if (stats.totalBuilds === 0) { + this.successfulBuildsPercentage = '-%'; + this.failedBuildsPercentage = '-%'; + this.cancelledBuildsPercentage = '-%'; + this.timeoutBuildsPercentage = '-%'; + this.missingBuildsPercentage = '-%'; + } else { + this.successfulBuildsPercentage = ((stats.successfulBuilds / stats.totalBuilds) * 100).toFixed(2) + '%'; + this.failedBuildsPercentage = ((stats.failedBuilds / stats.totalBuilds) * 100).toFixed(2) + '%'; + this.cancelledBuildsPercentage = ((stats.cancelledBuilds / stats.totalBuilds) * 100).toFixed(2) + '%'; + this.timeoutBuildsPercentage = ((stats.timeOutBuilds / stats.totalBuilds) * 100).toFixed(2) + '%'; + this.missingBuildsPercentage = ((stats.missingBuilds / stats.totalBuilds) * 100).toFixed(2) + '%'; + } + this.ngxData = [ + { name: 'Successful', value: stats.successfulBuilds }, + { name: 'Failed', value: stats.failedBuilds }, + { name: 'Cancelled', value: stats.cancelledBuilds }, + { name: 'Timeout', value: stats.timeOutBuilds }, + { name: 'Missing', value: stats.missingBuilds }, + ]; + } + + /** + * Callback function when the tab is changed + * @param span The new span + */ + onTabChange(span: SpanType): void { + if (this.currentSpan !== span) { + this.currentSpan = span; + this.getBuildJobStatistics(span); + } + } +} 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 bc447c21135d..67d59cf03b67 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 @@ -1,73 +1,4 @@ -
-
-
-

- -
- - - -
-
- -
-
-
-
-
-
-

{{ successfulBuildsPercentage }}

-

-
-
-
-
- - {{ buildJobStatistics.totalBuilds }} -
-
- - {{ buildJobStatistics.successfulBuilds }} -
-
-
-
- - {{ failedBuildsPercentage }} -
-
- - {{ buildJobStatistics.failedBuilds }} -
-
-
-
- - {{ cancelledBuildsPercentage }} -
-
- - {{ buildJobStatistics.cancelledBuilds }} -
-
-
-
- -
-
-
-
+

@@ -527,7 +458,7 @@

- + @@ -538,14 +469,10 @@

- + - - - - @@ -561,6 +488,9 @@

+ + + @@ -658,9 +588,6 @@

- {{ finishedBuildJob.repositoryName }} - {{ finishedBuildJob.repositoryType }} @@ -682,7 +609,7 @@

{{ finishedBuildJob.commitHash }}{{ finishedBuildJob.commitHash?.substring(0, 7) }} } @else if (finishedBuildJob.triggeredByPushTo === TriggeredByPushTo.USER) { {{ finishedBuildJob.commitHash }}{{ finishedBuildJob.commitHash?.substring(0, 7) }} } @else { - {{ finishedBuildJob.commitHash }} + {{ finishedBuildJob.commitHash?.substring(0, 7) }} } @@ -710,6 +637,9 @@

{{ finishedBuildJob.buildDuration }} + + {{ finishedBuildJob.buildSubmissionDate | artemisDate: 'long' : true }} + {{ finishedBuildJob.buildStartDate | artemisDate: 'long' : true }} @@ -810,7 +740,7 @@

- +
@@ -818,31 +748,31 @@
- +
- +
@if (!finishedBuildJobFilter.areDatesValid) { - + }
diff --git a/src/main/webapp/app/localci/build-queue/build-queue.component.scss b/src/main/webapp/app/localci/build-queue/build-queue.component.scss index 1989ed2fb6d9..4b8a3664cd45 100644 --- a/src/main/webapp/app/localci/build-queue/build-queue.component.scss +++ b/src/main/webapp/app/localci/build-queue/build-queue.component.scss @@ -14,15 +14,6 @@ width: 13%; } -.result-card { - width: 8vw; - min-width: 115px; -} - -.stats-column { - min-width: 230px; -} - .finish-jobs-column-tiny { max-width: 20px; } diff --git a/src/main/webapp/app/localci/build-queue/build-queue.component.ts b/src/main/webapp/app/localci/build-queue/build-queue.component.ts index 159eda1eee97..7b1d3329447e 100644 --- a/src/main/webapp/app/localci/build-queue/build-queue.component.ts +++ b/src/main/webapp/app/localci/build-queue/build-queue.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit, ViewChild, inject } from '@angular/core'; import { ActivatedRoute, RouterLink } from '@angular/router'; -import { BuildJob, BuildJobStatistics, FinishedBuildJob, SpanType } from 'app/entities/programming/build-job.model'; -import { faAngleDown, faAngleRight, faCircleCheck, faExclamationCircle, faExclamationTriangle, faFilter, faSort, faSync, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { BuildJob, FinishedBuildJob } from 'app/entities/programming/build-job.model'; +import { faCircleCheck, faExclamationCircle, faExclamationTriangle, faFilter, faSort, faSync, faTimes } from '@fortawesome/free-solid-svg-icons'; import { WebsocketService } from 'app/core/websocket/websocket.service'; import { BuildQueueService } from 'app/localci/build-queue/build-queue.service'; import { debounceTime, distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators'; @@ -12,10 +12,7 @@ import { onError } from 'app/shared/util/global.utils'; import { HttpErrorResponse, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; import dayjs from 'dayjs/esm'; -import { GraphColors } from 'app/entities/statistics.model'; -import { Color, PieChartModule, ScaleType } from '@swimlane/ngx-charts'; -import { NgxChartsSingleSeriesDataEntry } from 'app/shared/chart/ngx-charts-datatypes'; -import { NgbCollapse, NgbModal, NgbPagination, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; +import { NgbModal, NgbPagination, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; import { LocalStorageService } from 'ngx-webstorage'; import { Observable, OperatorFunction, Subject, Subscription, merge } from 'rxjs'; import { UI_RELOAD_TIME } from 'app/shared/constants/exercise-exam-constants'; @@ -29,18 +26,19 @@ import { NgClass } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { SortDirective } from 'app/shared/sort/sort.directive'; import { SortByDirective } from 'app/shared/sort/sort-by.directive'; -import { ResultComponent } from '../../exercises/shared/result/result.component'; +import { ResultComponent } from 'app/exercises/shared/result/result.component'; import { ItemCountComponent } from 'app/shared/pagination/item-count.component'; import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { ArtemisDurationFromSecondsPipe } from 'app/shared/pipes/artemis-duration-from-seconds.pipe'; +import { BuildJobStatisticsComponent } from 'app/localci/build-queue/build-job-statistics/build-job-statistics.component'; export class FinishedBuildJobFilter { status?: string = undefined; buildAgentAddress?: string = undefined; - buildStartDateFilterFrom?: dayjs.Dayjs = undefined; - buildStartDateFilterTo?: dayjs.Dayjs = undefined; + buildSubmissionDateFilterFrom?: dayjs.Dayjs = undefined; + buildSubmissionDateFilterTo?: dayjs.Dayjs = undefined; buildDurationFilterLowerBound?: number = undefined; buildDurationFilterUpperBound?: number = undefined; numberOfAppliedFilters = 0; @@ -59,11 +57,11 @@ export class FinishedBuildJobFilter { if (this.buildAgentAddress) { options = options.append('buildAgentAddress', this.buildAgentAddress); } - if (this.buildStartDateFilterFrom) { - options = options.append('startDate', this.buildStartDateFilterFrom.toISOString()); + if (this.buildSubmissionDateFilterFrom) { + options = options.append('startDate', this.buildSubmissionDateFilterFrom.toISOString()); } - if (this.buildStartDateFilterTo) { - options = options.append('endDate', this.buildStartDateFilterTo.toISOString()); + if (this.buildSubmissionDateFilterTo) { + options = options.append('endDate', this.buildSubmissionDateFilterTo.toISOString()); } if (this.buildDurationFilterLowerBound) { options = options.append('buildDurationLower', this.buildDurationFilterLowerBound.toString()); @@ -105,13 +103,17 @@ enum BuildJobStatusFilter { FAILED = 'failed', ERROR = 'error', CANCELLED = 'cancelled', + MISSING = 'missing', + BUILDING = 'building', + QUEUED = 'queued', + TIMEOUT = 'timeout', } export enum FinishedBuildJobFilterStorageKey { status = 'artemis.buildQueue.finishedBuildJobFilterStatus', buildAgentAddress = 'artemis.buildQueue.finishedBuildJobFilterBuildAgentAddress', - buildStartDateFilterFrom = 'artemis.buildQueue.finishedBuildJobFilterBuildStartDateFilterFrom', - buildStartDateFilterTo = 'artemis.buildQueue.finishedBuildJobFilterBuildStartDateFilterTo', + buildSubmissionDateFilterFrom = 'artemis.buildQueue.finishedBuildJobFilterBuildSubmissionDateFilterFrom', + buildSubmissionDateFilterTo = 'artemis.buildQueue.finishedBuildJobFilterBuildSubmissionDateFilterTo', buildDurationFilterLowerBound = 'artemis.buildQueue.finishedBuildJobFilterBuildDurationFilterLowerBound', buildDurationFilterUpperBound = 'artemis.buildQueue.finishedBuildJobFilterBuildDurationFilterUpperBound', } @@ -124,8 +126,6 @@ export enum FinishedBuildJobFilterStorageKey { TranslateDirective, HelpIconComponent, FaIconComponent, - NgbCollapse, - PieChartModule, DataTableComponent, NgxDatatableModule, NgClass, @@ -141,6 +141,7 @@ export enum FinishedBuildJobFilterStorageKey { ArtemisDatePipe, ArtemisTranslatePipe, ArtemisDurationFromSecondsPipe, + BuildJobStatisticsComponent, ], }) export class BuildQueueComponent implements OnInit, OnDestroy { @@ -157,7 +158,6 @@ export class BuildQueueComponent implements OnInit, OnDestroy { runningBuildJobs: BuildJob[] = []; finishedBuildJobs: FinishedBuildJob[] = []; courseChannels: string[] = []; - buildJobStatistics = new BuildJobStatistics(); //icons readonly faTimes = faTimes; @@ -166,31 +166,13 @@ export class BuildQueueComponent implements OnInit, OnDestroy { readonly faExclamationCircle = faExclamationCircle; readonly faExclamationTriangle = faExclamationTriangle; readonly faSync = faSync; - readonly faAngleDown = faAngleDown; - readonly faAngleRight = faAngleRight; - - protected readonly SpanType = SpanType; totalItems = 0; itemsPerPage = ITEMS_PER_PAGE; page = 1; - predicate = 'buildCompletionDate'; + predicate = 'buildSubmissionDate'; ascending = false; buildDurationInterval: ReturnType; - isCollapsed = false; - successfulBuildsPercentage: string; - failedBuildsPercentage: string; - cancelledBuildsPercentage: string; - currentSpan: SpanType = SpanType.WEEK; - - ngxData: NgxChartsSingleSeriesDataEntry[] = []; - - ngxColor = { - name: 'vivid', - selectable: true, - group: ScaleType.Ordinal, - domain: [GraphColors.GREEN, GraphColors.RED, GraphColors.YELLOW], - } as Color; // Filter @ViewChild('addressTypeahead', { static: true }) addressTypeahead: NgbTypeahead; @@ -213,7 +195,6 @@ export class BuildQueueComponent implements OnInit, OnDestroy { this.buildDurationInterval = setInterval(() => { this.runningBuildJobs = this.updateBuildJobDuration(this.runningBuildJobs); }, 1000); // 1 second - this.getBuildJobStatistics(this.currentSpan); this.loadFilterFromLocalStorage(); this.loadFinishedBuildJobs(); this.initWebsocketSubscription(); @@ -594,24 +575,24 @@ export class BuildQueueComponent implements OnInit, OnDestroy { * Method to remove the build start date filter and store the selected build start date in the local store if required. */ filterDateChanged() { - if (!this.finishedBuildJobFilter.buildStartDateFilterFrom?.isValid()) { - this.finishedBuildJobFilter.buildStartDateFilterFrom = undefined; - this.localStorage.clear(FinishedBuildJobFilterStorageKey.buildStartDateFilterFrom); - this.finishedBuildJobFilter.removeFilterFromFilterMap(FinishedBuildJobFilterStorageKey.buildStartDateFilterFrom); + if (!this.finishedBuildJobFilter.buildSubmissionDateFilterFrom?.isValid()) { + this.finishedBuildJobFilter.buildSubmissionDateFilterFrom = undefined; + this.localStorage.clear(FinishedBuildJobFilterStorageKey.buildSubmissionDateFilterFrom); + this.finishedBuildJobFilter.removeFilterFromFilterMap(FinishedBuildJobFilterStorageKey.buildSubmissionDateFilterFrom); } else { - this.localStorage.store(FinishedBuildJobFilterStorageKey.buildStartDateFilterFrom, this.finishedBuildJobFilter.buildStartDateFilterFrom); - this.finishedBuildJobFilter.addFilterToFilterMap(FinishedBuildJobFilterStorageKey.buildStartDateFilterFrom); + this.localStorage.store(FinishedBuildJobFilterStorageKey.buildSubmissionDateFilterFrom, this.finishedBuildJobFilter.buildSubmissionDateFilterFrom); + this.finishedBuildJobFilter.addFilterToFilterMap(FinishedBuildJobFilterStorageKey.buildSubmissionDateFilterFrom); } - if (!this.finishedBuildJobFilter.buildStartDateFilterTo?.isValid()) { - this.finishedBuildJobFilter.buildStartDateFilterTo = undefined; - this.localStorage.clear(FinishedBuildJobFilterStorageKey.buildStartDateFilterTo); - this.finishedBuildJobFilter.removeFilterFromFilterMap(FinishedBuildJobFilterStorageKey.buildStartDateFilterTo); + if (!this.finishedBuildJobFilter.buildSubmissionDateFilterTo?.isValid()) { + this.finishedBuildJobFilter.buildSubmissionDateFilterTo = undefined; + this.localStorage.clear(FinishedBuildJobFilterStorageKey.buildSubmissionDateFilterTo); + this.finishedBuildJobFilter.removeFilterFromFilterMap(FinishedBuildJobFilterStorageKey.buildSubmissionDateFilterTo); } else { - this.localStorage.store(FinishedBuildJobFilterStorageKey.buildStartDateFilterTo, this.finishedBuildJobFilter.buildStartDateFilterTo); - this.finishedBuildJobFilter.addFilterToFilterMap(FinishedBuildJobFilterStorageKey.buildStartDateFilterTo); + this.localStorage.store(FinishedBuildJobFilterStorageKey.buildSubmissionDateFilterTo, this.finishedBuildJobFilter.buildSubmissionDateFilterTo); + this.finishedBuildJobFilter.addFilterToFilterMap(FinishedBuildJobFilterStorageKey.buildSubmissionDateFilterTo); } - if (this.finishedBuildJobFilter.buildStartDateFilterFrom && this.finishedBuildJobFilter.buildStartDateFilterTo) { - this.finishedBuildJobFilter.areDatesValid = this.finishedBuildJobFilter.buildStartDateFilterFrom.isBefore(this.finishedBuildJobFilter.buildStartDateFilterTo); + if (this.finishedBuildJobFilter.buildSubmissionDateFilterFrom && this.finishedBuildJobFilter.buildSubmissionDateFilterTo) { + this.finishedBuildJobFilter.areDatesValid = this.finishedBuildJobFilter.buildSubmissionDateFilterFrom.isBefore(this.finishedBuildJobFilter.buildSubmissionDateFilterTo); } else { this.finishedBuildJobFilter.areDatesValid = true; } @@ -642,65 +623,4 @@ export class BuildQueueComponent implements OnInit, OnDestroy { this.finishedBuildJobFilter.areDurationFiltersValid = true; } } - - /** - * Get Build Job Result statistics. Should be called in admin view only. - */ - getBuildJobStatistics(span: SpanType = SpanType.WEEK) { - this.route.paramMap.pipe(take(1)).subscribe((params) => { - const courseId = Number(params.get('courseId')); - if (courseId) { - this.buildQueueService.getBuildJobStatisticsForCourse(courseId, span).subscribe({ - next: (res: BuildJobStatistics) => { - this.updateDisplayedBuildJobStatistics(res); - }, - error: (res: HttpErrorResponse) => { - onError(this.alertService, res); - }, - }); - } else { - this.buildQueueService.getBuildJobStatistics(span).subscribe({ - next: (res: BuildJobStatistics) => { - this.updateDisplayedBuildJobStatistics(res); - }, - error: (res: HttpErrorResponse) => { - onError(this.alertService, res); - }, - }); - } - }); - } - - /** - * Update the displayed build job statistics - * @param stats The new build job statistics - */ - updateDisplayedBuildJobStatistics(stats: BuildJobStatistics) { - this.buildJobStatistics = stats; - if (stats.totalBuilds === 0) { - this.successfulBuildsPercentage = '-%'; - this.failedBuildsPercentage = '-%'; - this.cancelledBuildsPercentage = '-%'; - } else { - this.successfulBuildsPercentage = ((stats.successfulBuilds / stats.totalBuilds) * 100).toFixed(2) + '%'; - this.failedBuildsPercentage = ((stats.failedBuilds / stats.totalBuilds) * 100).toFixed(2) + '%'; - this.cancelledBuildsPercentage = ((stats.cancelledBuilds / stats.totalBuilds) * 100).toFixed(2) + '%'; - } - this.ngxData = [ - { name: 'Successful', value: stats.successfulBuilds }, - { name: 'Failed', value: stats.failedBuilds }, - { name: 'Cancelled', value: stats.cancelledBuilds }, - ]; - } - - /** - * Callback function when the tab is changed - * @param span The new span - */ - onTabChange(span: SpanType): void { - if (this.currentSpan !== span) { - this.currentSpan = span; - this.getBuildJobStatistics(span); - } - } } diff --git a/src/main/webapp/i18n/de/buildQueue.json b/src/main/webapp/i18n/de/buildQueue.json index 7d56dc944003..db81856f58ed 100644 --- a/src/main/webapp/i18n/de/buildQueue.json +++ b/src/main/webapp/i18n/de/buildQueue.json @@ -34,11 +34,15 @@ "cancelled": "Abgebrochen", "failed": "Fehlgeschlagen", "error": "Fehler", - "successful": "Erfolgreich" + "successful": "Erfolgreich", + "queued": "In Warteschlange", + "missing": "Fehlend", + "building": "In Bearbeitung", + "timeout": "Zeitüberschreitung" }, "buildAgentAddress": "Build Agent Adresse", - "buildStartDate": { - "title": "Build Startdatum", + "buildSubmissionDate": { + "title": "Build Abgabedatum", "from": "Von", "to": "Bis", "invalidDate": "Von-Datum muss vor dem Bis-Datum liegen" @@ -71,6 +75,10 @@ "failedBuildsPercentage": "Fehlgeschlagene Builds %:", "cancelledBuilds": "Abgebrochene Builds:", "cancelledBuildsPercentage": "Abgebrochene Builds %:", + "missingBuilds": "Fehlende Builds:", + "missingBuildsPercentage": "Fehlende Builds %:", + "timeoutBuilds": "Zeitüberschreitende Builds:", + "timeoutBuildsPercentage": "Zeitüberschreitende Builds %:", "daySpan": "1 Tag", "weekSpan": "7 Tage", "monthSpan": "30 Tage" diff --git a/src/main/webapp/i18n/de/result.json b/src/main/webapp/i18n/de/result.json index 933db7e69593..04adaebf3e82 100644 --- a/src/main/webapp/i18n/de/result.json +++ b/src/main/webapp/i18n/de/result.json @@ -122,7 +122,7 @@ "scaIssueCount": "Du hast {{issues}} Code-Issues.", "manualFeedbackCount": "Du hast {{feedbacks}} manuelle Feedbacks bekommen.", "buildLogs": { - "viewLogs": "Build-Logs anzeigen" + "viewLogs": "Logs anzeigen" } } } diff --git a/src/main/webapp/i18n/en/buildQueue.json b/src/main/webapp/i18n/en/buildQueue.json index 63a258d407d1..3b37292302f6 100644 --- a/src/main/webapp/i18n/en/buildQueue.json +++ b/src/main/webapp/i18n/en/buildQueue.json @@ -34,11 +34,15 @@ "cancelled": "Cancelled", "failed": "Failed", "error": "Error", - "successful": "Successful" + "successful": "Successful", + "building": "Building", + "queued": "Queued", + "missing": "Missing", + "timeout": "Timed out" }, "buildAgentAddress": "Build Agent Address", - "buildStartDate": { - "title": "Build Start Date", + "buildSubmissionDate": { + "title": "Build Submission Date", "from": "From", "to": "To", "invalidDate": "From date must be before to date" @@ -71,6 +75,10 @@ "failedBuildsPercentage": "Failed Builds %:", "cancelledBuilds": "Cancelled Builds:", "cancelledBuildsPercentage": "Cancelled Builds %:", + "missingBuilds": "Missing Builds:", + "missingBuildsPercentage": "Missing Builds %:", + "timeoutBuilds": "Timed out Builds:", + "timeoutBuildsPercentage": "Timed out Builds %:", "daySpan": "1 day", "weekSpan": "7 days", "monthSpan": "30 days" diff --git a/src/main/webapp/i18n/en/result.json b/src/main/webapp/i18n/en/result.json index f8178c0cde1e..2887436b8fc3 100644 --- a/src/main/webapp/i18n/en/result.json +++ b/src/main/webapp/i18n/en/result.json @@ -122,7 +122,7 @@ "scaIssueCount": "You have {{issues}} code issues.", "manualFeedbackCount": "You received {{feedbacks}} manual feedbacks.", "buildLogs": { - "viewLogs": "View build logs" + "viewLogs": "View logs" } } } 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 index 56adec3eb361..065a5c3fabc3 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationLocalCILocalVCTest.java @@ -21,6 +21,7 @@ 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.LocalCIEventListenerService; 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; @@ -79,6 +80,9 @@ public abstract class AbstractProgrammingIntegrationLocalCILocalVCTest extends A @Autowired protected SharedQueueProcessingService sharedQueueProcessingService; + @Autowired + protected LocalCIEventListenerService localCIEventListenerService; + @Autowired protected StaticCodeAnalysisService staticCodeAnalysisService; 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 52ccfd718e22..b3d35a60707a 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 @@ -28,6 +28,9 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.transport.CredentialsProvider; @@ -49,6 +52,8 @@ import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.command.CopyArchiveFromContainerCmd; import com.github.dockerjava.api.command.ExecStartCmd; +import com.github.dockerjava.api.command.InspectImageCmd; +import com.github.dockerjava.api.command.InspectImageResponse; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.Frame; import com.hazelcast.collection.IQueue; @@ -64,6 +69,7 @@ import de.tum.cit.aet.artemis.exercise.domain.Team; import de.tum.cit.aet.artemis.exercise.dto.SubmissionDTO; import de.tum.cit.aet.artemis.programming.AbstractProgrammingIntegrationLocalCILocalVCTestBase; +import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildConfig; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseBuildStatistics; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; @@ -153,6 +159,9 @@ void testSubmitViaOnlineEditor() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testBuildJobPersistence() { + // Stop the build agent to prevent the build job from being processed. + sharedQueueProcessingService.removeListenerAndCancelScheduledFuture(); + ProgrammingExerciseStudentParticipation studentParticipation = localVCLocalCITestService.createParticipation(programmingExercise, student1Login); localVCServletService.processNewPush(commitHash, studentAssignmentRepository.originGit.getRepository()); @@ -166,7 +175,7 @@ void testBuildJobPersistence() { BuildJob buildJob = buildJobOptional.orElseThrow(); - assertThat(buildJob.getBuildStatus()).isEqualTo(BuildStatus.SUCCESSFUL); + assertThat(buildJob.getBuildStatus()).isEqualTo(BuildStatus.QUEUED); assertThat(buildJob.getRepositoryType()).isEqualTo(RepositoryType.USER); assertThat(buildJob.getCommitHash()).isEqualTo(commitHash); assertThat(buildJob.getTriggeredByPushTo()).isEqualTo(RepositoryType.USER); @@ -175,12 +184,122 @@ void testBuildJobPersistence() { assertThat(buildJob.getParticipationId()).isEqualTo(studentParticipation.getId()); assertThat(buildJob.getDockerImage()).isEqualTo(programmingExercise.getBuildConfig().getWindfile().metadata().docker().getFullImageName()); assertThat(buildJob.getRepositoryName()).isEqualTo(assignmentRepositorySlug); - assertThat(buildJob.getBuildAgentAddress()).isNotEmpty(); assertThat(buildJob.getPriority()).isEqualTo(2); assertThat(buildJob.getRetryCount()).isEqualTo(0); assertThat(buildJob.getName()).isNotEmpty(); + assertThat(buildJob.getBuildAgentAddress()).isNull(); + assertThat(buildJob.getBuildStartDate()).isNull(); + assertThat(buildJob.getBuildCompletionDate()).isNull(); + + // resume the build agent + sharedQueueProcessingService.init(); + + await().atMost(5, TimeUnit.SECONDS).until(() -> { + Optional buildJobOptionalTemp = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(studentParticipation.getId()); + return buildJobOptionalTemp.isPresent() && buildJobOptionalTemp.get().getBuildStatus() == BuildStatus.BUILDING; + }); + + await().atMost(15, TimeUnit.SECONDS).until(() -> { + Optional buildJobOptionalTemp = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(studentParticipation.getId()); + return buildJobOptionalTemp.isPresent() && buildJobOptionalTemp.get().getBuildStatus() == BuildStatus.SUCCESSFUL; + }); + + buildJobOptional = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(studentParticipation.getId()); + buildJob = buildJobOptional.orElseThrow(); + + assertThat(buildJob.getBuildStatus()).isEqualTo(BuildStatus.SUCCESSFUL); assertThat(buildJob.getBuildStartDate()).isNotNull(); assertThat(buildJob.getBuildCompletionDate()).isNotNull(); + assertThat(buildJob.getBuildAgentAddress()).isNotEmpty(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testBuildJobTimeoutPersistence() { + try (ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1)) { + ProgrammingExerciseBuildConfig buildConfig = programmingExercise.getBuildConfig(); + int originalTimeout = buildConfig.getTimeoutSeconds(); + buildConfig.setTimeoutSeconds(1); + programmingExerciseBuildConfigRepository.save(buildConfig); + + // delay the inspectImageCmd.exec() method by 1 second to simulate a timeout + InspectImageCmd inspectImageCmd = mock(InspectImageCmd.class); + InspectImageResponse inspectImageResponse = new InspectImageResponse(); + doReturn(inspectImageCmd).when(dockerClient).inspectImageCmd(anyString()); + doAnswer(invocation -> { + var future = scheduler.schedule(() -> inspectImageResponse, 3, TimeUnit.SECONDS); + return future.get(4, TimeUnit.SECONDS); + }).when(inspectImageCmd).exec(); + + ProgrammingExerciseStudentParticipation studentParticipation = localVCLocalCITestService.createParticipation(programmingExercise, student1Login); + + localVCServletService.processNewPush(commitHash, studentAssignmentRepository.originGit.getRepository()); + + await().until(() -> { + Optional buildJobOptional = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(studentParticipation.getId()); + return buildJobOptional.isPresent() && buildJobOptional.get().getBuildStatus() != BuildStatus.BUILDING + && buildJobOptional.get().getBuildStatus() != BuildStatus.QUEUED; + }); + + Optional buildJobOptional = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(studentParticipation.getId()); + + BuildJob buildJob = buildJobOptional.orElseThrow(); + + assertThat(buildJob.getBuildStatus()).isEqualTo(BuildStatus.TIMEOUT); + assertThat(buildJob.getRepositoryType()).isEqualTo(RepositoryType.USER); + assertThat(buildJob.getCommitHash()).isEqualTo(commitHash); + assertThat(buildJob.getTriggeredByPushTo()).isEqualTo(RepositoryType.USER); + assertThat(buildJob.getCourseId()).isEqualTo(course.getId()); + assertThat(buildJob.getExerciseId()).isEqualTo(programmingExercise.getId()); + assertThat(buildJob.getParticipationId()).isEqualTo(studentParticipation.getId()); + assertThat(buildJob.getDockerImage()).isEqualTo(programmingExercise.getBuildConfig().getWindfile().metadata().docker().getFullImageName()); + assertThat(buildJob.getRepositoryName()).isEqualTo(assignmentRepositorySlug); + assertThat(buildJob.getPriority()).isEqualTo(2); + assertThat(buildJob.getRetryCount()).isEqualTo(0); + assertThat(buildJob.getName()).isNotEmpty(); + assertThat(buildJob.getBuildStartDate()).isNotNull(); + assertThat(buildJob.getBuildCompletionDate()).isNotNull(); + assertThat(buildJob.getBuildAgentAddress()).isNotEmpty(); + + buildConfig.setTimeoutSeconds(originalTimeout); + programmingExerciseBuildConfigRepository.save(buildConfig); + } + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testMissingBuildJobCheck() { + // Stop the build agent to prevent the build job from being processed. + sharedQueueProcessingService.removeListenerAndCancelScheduledFuture(); + + ProgrammingExerciseStudentParticipation studentParticipation = localVCLocalCITestService.createParticipation(programmingExercise, student1Login); + + localVCServletService.processNewPush(commitHash, studentAssignmentRepository.originGit.getRepository()); + + await().until(() -> { + Optional buildJobOptional = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(studentParticipation.getId()); + return buildJobOptional.isPresent() && buildJobOptional.get().getBuildStatus() == BuildStatus.QUEUED; + }); + + Optional buildJobOptional = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(studentParticipation.getId()); + + BuildJob buildJob = buildJobOptional.orElseThrow(); + + assertThat(buildJob.getBuildStatus()).isEqualTo(BuildStatus.QUEUED); + + buildJob.setBuildSubmissionDate(ZonedDateTime.now().minusMinutes(6)); + buildJobRepository.save(buildJob); + + hazelcastInstance.getQueue("buildJobQueue").clear(); + + localCIEventListenerService.checkPendingBuildJobsStatus(); + + buildJobOptional = buildJobRepository.findFirstByParticipationIdOrderByBuildStartDateDesc(studentParticipation.getId()); + buildJob = buildJobOptional.orElseThrow(); + assertThat(buildJob.getBuildStatus()).isEqualTo(BuildStatus.MISSING); + + // resume the build agent + sharedQueueProcessingService.init(); } @Test 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 eb03d08f5fd8..5d1dfc2ba023 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 @@ -299,8 +299,8 @@ void testGetFinishedBuildJobs_returnsFilteredJobs() throws Exception { PageableSearchDTO pageableSearchDTO = pageableSearchUtilService.configureFinishedJobsSearchDTO(); LinkedMultiValueMap searchParams = pageableSearchUtilService.searchMapping(pageableSearchDTO, "pageable"); searchParams.add("buildStatus", "FAILED"); - searchParams.add("startDate", jobTimingInfo.buildStartDate().minusSeconds(10).toString()); - searchParams.add("endDate", jobTimingInfo.buildCompletionDate().plusSeconds(10).toString()); + searchParams.add("startDate", jobTimingInfo.submissionDate().minusSeconds(10).toString()); + searchParams.add("endDate", jobTimingInfo.submissionDate().plusSeconds(10).toString()); searchParams.add("searchTerm", "short"); searchParams.add("buildDurationLower", "120"); searchParams.add("buildDurationUpper", "600"); diff --git a/src/test/javascript/spec/component/localci/build-queue/build-job-statistics/build-job-statistics.component.spec.ts b/src/test/javascript/spec/component/localci/build-queue/build-job-statistics/build-job-statistics.component.spec.ts new file mode 100644 index 000000000000..9c53f69e4e50 --- /dev/null +++ b/src/test/javascript/spec/component/localci/build-queue/build-job-statistics/build-job-statistics.component.spec.ts @@ -0,0 +1,80 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BuildJobStatisticsComponent } from '../../../../../../../main/webapp/app/localci/build-queue/build-job-statistics/build-job-statistics.component'; +import { BuildJobStatistics, SpanType } from '../../../../../../../main/webapp/app/entities/programming/build-job.model'; +import { ArtemisTestModule } from '../../../../test.module'; +import { BuildQueueService } from '../../../../../../../main/webapp/app/localci/build-queue/build-queue.service'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; + +describe('BuildJobStatisticsComponent', () => { + let component: BuildJobStatisticsComponent; + let fixture: ComponentFixture; + const mockActivatedRoute: any = {}; + + const mockBuildQueueService = { + getBuildJobStatistics: jest.fn(), + getBuildJobStatisticsForCourse: jest.fn(), + }; + + const mockBuildJobStatistics: BuildJobStatistics = { + totalBuilds: 15, + successfulBuilds: 5, + failedBuilds: 3, + cancelledBuilds: 2, + timeOutBuilds: 3, + missingBuilds: 2, + }; + + const testCourseId = 123; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ArtemisTestModule, BuildJobStatisticsComponent], + providers: [ + { provide: BuildQueueService, useValue: mockBuildQueueService }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + ], + }).compileComponents(); + + mockActivatedRoute.paramMap = of(new Map([])); + + fixture = TestBed.createComponent(BuildJobStatisticsComponent); + component = fixture.componentInstance; + }); + + beforeEach(() => { + mockBuildQueueService.getBuildJobStatistics.mockClear(); + }); + + it('should get build job statistics when changing the span', () => { + mockBuildQueueService.getBuildJobStatistics.mockReturnValue(of(mockBuildJobStatistics)); + + component.ngOnInit(); + component.onTabChange(SpanType.DAY); + + expect(mockBuildQueueService.getBuildJobStatistics).toHaveBeenCalledTimes(2); + expect(component.buildJobStatistics).toEqual(mockBuildJobStatistics); + }); + + it('should not get build job statistics when span is the same', () => { + mockBuildQueueService.getBuildJobStatistics.mockReturnValue(of(mockBuildJobStatistics)); + + component.ngOnInit(); + component.onTabChange(SpanType.WEEK); + + expect(mockBuildQueueService.getBuildJobStatistics).toHaveBeenCalledOnce(); + expect(component.buildJobStatistics).toEqual(mockBuildJobStatistics); + }); + + it('should get build job statistics for course when courseId is present', () => { + mockActivatedRoute.paramMap = of(new Map([['courseId', testCourseId]])); + mockBuildQueueService.getBuildJobStatisticsForCourse.mockReturnValue(of(mockBuildJobStatistics)); + + component.ngOnInit(); + component.onTabChange(SpanType.WEEK); + + expect(mockBuildQueueService.getBuildJobStatisticsForCourse).toHaveBeenNthCalledWith(1, testCourseId, SpanType.WEEK); + expect(component.buildJobStatistics).toEqual(mockBuildJobStatistics); + }); +}); diff --git a/src/test/javascript/spec/component/localci/build-queue/build-queue.component.spec.ts b/src/test/javascript/spec/component/localci/build-queue/build-queue.component.spec.ts index 6cda0e4df750..b0a5e73324cf 100644 --- a/src/test/javascript/spec/component/localci/build-queue/build-queue.component.spec.ts +++ b/src/test/javascript/spec/component/localci/build-queue/build-queue.component.spec.ts @@ -7,7 +7,7 @@ import dayjs from 'dayjs/esm'; import { AccountService } from 'app/core/auth/account.service'; import { DataTableComponent } from 'app/shared/data-table/data-table.component'; import { ArtemisTestModule } from '../../../test.module'; -import { BuildJobStatistics, FinishedBuildJob, SpanType } from 'app/entities/programming/build-job.model'; +import { FinishedBuildJob } from 'app/entities/programming/build-job.model'; import { TriggeredByPushTo } from 'app/entities/programming/repository-info.model'; import { HttpResponse } from '@angular/common/http'; import { SortingOrder } from 'app/shared/table/pageable-table'; @@ -211,6 +211,7 @@ describe('BuildQueueComponent', () => { repositoryName: 'repo5', repositoryType: 'USER', triggeredByPushTo: TriggeredByPushTo.USER, + buildSubmissionDate: dayjs('2023-01-05'), buildStartDate: dayjs('2023-01-05'), buildCompletionDate: dayjs('2023-01-05'), buildDuration: undefined, @@ -235,19 +236,12 @@ describe('BuildQueueComponent', () => { }, ]; - const mockBuildJobStatistics: BuildJobStatistics = { - totalBuilds: 10, - successfulBuilds: 5, - failedBuilds: 3, - cancelledBuilds: 2, - }; - const mockFinishedJobsResponse: HttpResponse = new HttpResponse({ body: mockFinishedJobs }); const request = { page: 1, pageSize: 50, - sortedColumn: 'buildCompletionDate', + sortedColumn: 'buildSubmissionDate', sortingOrder: SortingOrder.DESCENDING, searchTerm: '', }; @@ -280,7 +274,7 @@ describe('BuildQueueComponent', () => { mockActivatedRoute = { params: of({ courseId: testCourseId }) }; TestBed.configureTestingModule({ - imports: [ArtemisTestModule], + imports: [ArtemisTestModule, BuildQueueComponent], providers: [ { provide: BuildQueueService, useValue: mockBuildQueueService }, { provide: ActivatedRoute, useValue: mockActivatedRoute }, @@ -317,7 +311,6 @@ describe('BuildQueueComponent', () => { mockBuildQueueService.getQueuedBuildJobs.mockReturnValue(of(mockQueuedJobs)); mockBuildQueueService.getRunningBuildJobs.mockReturnValue(of(mockRunningJobs)); mockBuildQueueService.getFinishedBuildJobs.mockReturnValue(of(mockFinishedJobsResponse)); - mockBuildQueueService.getBuildJobStatistics.mockReturnValue(of(mockBuildJobStatistics)); // Initialize the component component.ngOnInit(); @@ -346,7 +339,6 @@ describe('BuildQueueComponent', () => { mockBuildQueueService.getQueuedBuildJobsByCourseId.mockReturnValue(of(mockQueuedJobs)); mockBuildQueueService.getRunningBuildJobsByCourseId.mockReturnValue(of(mockRunningJobs)); mockBuildQueueService.getFinishedBuildJobsByCourseId.mockReturnValue(of(mockFinishedJobsResponse)); - mockBuildQueueService.getBuildJobStatisticsForCourse.mockReturnValue(of(mockBuildJobStatistics)); // Initialize the component component.ngOnInit(); @@ -355,39 +347,11 @@ describe('BuildQueueComponent', () => { expect(mockBuildQueueService.getQueuedBuildJobsByCourseId).toHaveBeenCalledWith(testCourseId); expect(mockBuildQueueService.getRunningBuildJobsByCourseId).toHaveBeenCalledWith(testCourseId); expect(mockBuildQueueService.getFinishedBuildJobsByCourseId).toHaveBeenCalledWith(testCourseId, request, filterOptionsEmpty); - expect(mockBuildQueueService.getBuildJobStatisticsForCourse).toHaveBeenCalledWith(testCourseId, SpanType.WEEK); // Expectations: The component's properties are set with the mock data expect(component.queuedBuildJobs).toEqual(mockQueuedJobs); expect(component.runningBuildJobs).toEqual(mockRunningJobs); expect(component.finishedBuildJobs).toEqual(mockFinishedJobs); - expect(component.buildJobStatistics).toEqual(mockBuildJobStatistics); - }); - - it('should get build job statistics when changing the span', () => { - // Mock ActivatedRoute to return no course ID - mockActivatedRoute.paramMap = of(new Map([])); - - mockBuildQueueService.getBuildJobStatistics.mockReturnValue(of(mockBuildJobStatistics)); - - component.ngOnInit(); - component.onTabChange(SpanType.DAY); - - expect(mockBuildQueueService.getBuildJobStatistics).toHaveBeenCalledTimes(2); - expect(component.buildJobStatistics).toEqual(mockBuildJobStatistics); - }); - - it('should not get build job statistics when span is the same', () => { - // Mock ActivatedRoute to return no course ID - mockActivatedRoute.paramMap = of(new Map([])); - - mockBuildQueueService.getBuildJobStatistics.mockReturnValue(of(mockBuildJobStatistics)); - - component.ngOnInit(); - component.onTabChange(SpanType.WEEK); - - expect(mockBuildQueueService.getBuildJobStatistics).toHaveBeenCalledOnce(); - expect(component.buildJobStatistics).toEqual(mockBuildJobStatistics); }); it('should refresh data', () => { @@ -593,8 +557,8 @@ describe('BuildQueueComponent', () => { component.finishedBuildJobFilter.buildDurationFilterLowerBound = 1; component.finishedBuildJobFilter.buildDurationFilterUpperBound = 2; component.filterDurationChanged(); - component.finishedBuildJobFilter.buildStartDateFilterFrom = dayjs('2023-01-01'); - component.finishedBuildJobFilter.buildStartDateFilterTo = dayjs('2023-01-02'); + component.finishedBuildJobFilter.buildSubmissionDateFilterFrom = dayjs('2023-01-01'); + component.finishedBuildJobFilter.buildSubmissionDateFilterTo = dayjs('2023-01-02'); component.filterDateChanged(); component.toggleBuildStatusFilter('SUCCESSFUL'); @@ -613,8 +577,8 @@ describe('BuildQueueComponent', () => { component.finishedBuildJobFilter.buildAgentAddress = 'agent1'; component.finishedBuildJobFilter.buildDurationFilterLowerBound = 1; component.finishedBuildJobFilter.buildDurationFilterUpperBound = 2; - component.finishedBuildJobFilter.buildStartDateFilterFrom = dayjs('2023-01-01'); - component.finishedBuildJobFilter.buildStartDateFilterTo = dayjs('2023-01-02'); + component.finishedBuildJobFilter.buildSubmissionDateFilterFrom = dayjs('2023-01-01'); + component.finishedBuildJobFilter.buildSubmissionDateFilterTo = dayjs('2023-01-02'); component.finishedBuildJobFilter.status = 'SUCCESSFUL'; component.filterDurationChanged(); @@ -625,8 +589,8 @@ describe('BuildQueueComponent', () => { expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildAgentAddress)).toBe('agent1'); expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildDurationFilterLowerBound)).toBe(1); expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildDurationFilterUpperBound)).toBe(2); - expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildStartDateFilterFrom)).toEqual(dayjs('2023-01-01')); - expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildStartDateFilterTo)).toEqual(dayjs('2023-01-02')); + expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildSubmissionDateFilterFrom)).toEqual(dayjs('2023-01-01')); + expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildSubmissionDateFilterTo)).toEqual(dayjs('2023-01-02')); expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.status)).toBe('SUCCESSFUL'); component.finishedBuildJobFilter = new FinishedBuildJobFilter(); @@ -639,15 +603,15 @@ describe('BuildQueueComponent', () => { expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildAgentAddress)).toBeUndefined(); expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildDurationFilterLowerBound)).toBeUndefined(); expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildDurationFilterUpperBound)).toBeUndefined(); - expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildStartDateFilterFrom)).toBeUndefined(); - expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildStartDateFilterTo)).toBeUndefined(); + expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildSubmissionDateFilterFrom)).toBeUndefined(); + expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.buildSubmissionDateFilterTo)).toBeUndefined(); expect(mockLocalStorageService.retrieve(FinishedBuildJobFilterStorageKey.status)).toBeUndefined(); }); it('should validate correctly', () => { component.finishedBuildJobFilter = new FinishedBuildJobFilter(); component.finishedBuildJobFilter.buildDurationFilterLowerBound = 1; - component.finishedBuildJobFilter.buildStartDateFilterFrom = dayjs('2023-01-01'); + component.finishedBuildJobFilter.buildSubmissionDateFilterFrom = dayjs('2023-01-01'); component.filterDurationChanged(); component.filterDateChanged(); @@ -655,7 +619,7 @@ describe('BuildQueueComponent', () => { expect(component.finishedBuildJobFilter.areDurationFiltersValid).toBeTruthy(); component.finishedBuildJobFilter.buildDurationFilterUpperBound = 2; - component.finishedBuildJobFilter.buildStartDateFilterTo = dayjs('2023-01-02'); + component.finishedBuildJobFilter.buildSubmissionDateFilterTo = dayjs('2023-01-02'); component.filterDurationChanged(); component.filterDateChanged(); @@ -663,7 +627,7 @@ describe('BuildQueueComponent', () => { expect(component.finishedBuildJobFilter.areDurationFiltersValid).toBeTruthy(); component.finishedBuildJobFilter.buildDurationFilterLowerBound = 3; - component.finishedBuildJobFilter.buildStartDateFilterFrom = dayjs('2023-01-03'); + component.finishedBuildJobFilter.buildSubmissionDateFilterFrom = dayjs('2023-01-03'); component.filterDurationChanged(); component.filterDateChanged(); diff --git a/src/test/javascript/spec/component/localci/build-queue/build-queue.service.spec.ts b/src/test/javascript/spec/component/localci/build-queue/build-queue.service.spec.ts index 8f5a9af490f5..fc796b51a061 100644 --- a/src/test/javascript/spec/component/localci/build-queue/build-queue.service.spec.ts +++ b/src/test/javascript/spec/component/localci/build-queue/build-queue.service.spec.ts @@ -29,8 +29,8 @@ describe('BuildQueueService', () => { filterOptions.buildAgentAddress = '[127.0.0.1]:5701'; filterOptions.buildDurationFilterLowerBound = 1; filterOptions.buildDurationFilterUpperBound = 10; - filterOptions.buildStartDateFilterFrom = dayjs('2024-01-01'); - filterOptions.buildStartDateFilterTo = dayjs('2024-01-02'); + filterOptions.buildSubmissionDateFilterFrom = dayjs('2024-01-01'); + filterOptions.buildSubmissionDateFilterTo = dayjs('2024-01-02'); filterOptions.status = 'SUCCESSFUL'; const buildLogEntries: BuildLogEntry[] = [ @@ -48,8 +48,8 @@ describe('BuildQueueService', () => { expect(req.request.params.get('buildAgentAddress')).toBe(filterOptions.buildAgentAddress); expect(req.request.params.get('buildDurationLower')).toBe(filterOptions.buildDurationFilterLowerBound?.toString()); expect(req.request.params.get('buildDurationUpper')).toBe(filterOptions.buildDurationFilterUpperBound?.toString()); - expect(req.request.params.get('startDate')).toBe(filterOptions.buildStartDateFilterFrom?.toISOString()); - expect(req.request.params.get('endDate')).toBe(filterOptions.buildStartDateFilterTo?.toISOString()); + expect(req.request.params.get('startDate')).toBe(filterOptions.buildSubmissionDateFilterFrom?.toISOString()); + expect(req.request.params.get('endDate')).toBe(filterOptions.buildSubmissionDateFilterTo?.toISOString()); expect(req.request.params.get('buildStatus')).toBe(filterOptions.status); }; @@ -538,7 +538,7 @@ describe('BuildQueueService', () => { }); it('should return build job statistics', fakeAsync(() => { - const expectedResponse: BuildJobStatistics = { totalBuilds: 1, successfulBuilds: 1, failedBuilds: 0, cancelledBuilds: 0 }; + const expectedResponse: BuildJobStatistics = { totalBuilds: 1, successfulBuilds: 1, failedBuilds: 0, cancelledBuilds: 0, timeOutBuilds: 0, missingBuilds: 0 }; service.getBuildJobStatistics(SpanType.WEEK).subscribe((data) => { expect(data).toEqual(expectedResponse);