Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development: Improve View Build Logs code #10180

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_LOCALCI;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
Expand All @@ -18,7 +16,6 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import de.tum.cit.aet.artemis.buildagent.dto.BuildLogDTO;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastEditor;
import de.tum.cit.aet.artemis.programming.service.BuildLogEntryService;

Expand Down Expand Up @@ -55,25 +52,4 @@ public ResponseEntity<Resource> getBuildLogForBuildJob(@PathVariable String buil
responseHeaders.setContentDispositionFormData("attachment", "build-" + buildJobId + ".log");
return new ResponseEntity<>(buildLog, responseHeaders, HttpStatus.OK);
}

/**
* GET /build-log/{buildJobId}/entries : get the build log entries for a given result
*
* @param buildJobId the id of the build job for which to retrieve the build log entries
* @return the ResponseEntity with status 200 (OK) and the build log entries in the body, or with status 404 (Not Found) if the build log entries could not be found
*/
@GetMapping("build-log/{buildJobId}/entries")
@EnforceAtLeastEditor
public ResponseEntity<List<BuildLogDTO>> getBuildLogEntriesForBuildJob(@PathVariable String buildJobId) {
FileSystemResource buildLog = buildLogEntryService.retrieveBuildLogsFromFileForBuildJob(buildJobId);
if (buildLog == null) {
return ResponseEntity.notFound().build();
}

var buildLogEntries = buildLogEntryService.parseBuildLogEntries(buildLog);
if (buildLogEntries == null || buildLogEntries.isEmpty()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(buildLogEntries);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export enum BuildLogType {
}

export type BuildLogLines = {
time: any;
time?: string;
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
logLines: string[];
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -891,22 +891,10 @@ <h5 class="modal-title">
<div class="modal-body">
<table class="table table-borderless">
<tbody>
@for (logEntry of rawBuildLogs; track logEntry) {
<tr class="build-output__entry">
<td class="build-output__entry-date">{{ logEntry.time | artemisDate: 'long' : true : undefined : false : true }}</td>
<td class="build-output__entry-text">
@for (line of logEntry.logLines; track line) {
<span>{{ line }}</span>
<br />
}
</td>
</tr>
} @empty {
<tr class="build-output__entry">
<td class="build-output__entry-text">
<span jhiTranslate="artemisApp.buildQueue.logs.noLogs"></span>
</td>
</tr>
@if (rawBuildLogsString) {
<pre>{{ rawBuildLogsString }}</pre>
} @else {
<span jhiTranslate="artemisApp.buildQueue.logs.noLogs"></span>
}
</tbody>
</table>
Expand Down
30 changes: 19 additions & 11 deletions src/main/webapp/app/localci/build-queue/build-queue.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { NgbCollapse, NgbModal, NgbPagination, NgbTypeahead } from '@ng-bootstra
import { LocalStorageService } from 'ngx-webstorage';
import { Observable, OperatorFunction, Subject, Subscription, merge } from 'rxjs';
import { UI_RELOAD_TIME } from 'app/shared/constants/exercise-exam-constants';
import { BuildLogEntry, BuildLogLines } from 'app/entities/programming/build-log.model';
import { TranslateDirective } from 'app/shared/language/translate.directive';
import { HelpIconComponent } from 'app/shared/components/help-icon.component';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
Expand Down Expand Up @@ -204,8 +203,8 @@ export class BuildQueueComponent implements OnInit, OnDestroy {
searchSubscription: Subscription;
searchTerm?: string = undefined;

rawBuildLogs: BuildLogLines[] = [];
displayedBuildJobId?: string;
rawBuildLogsString: string = '';

ngOnInit() {
this.buildStatusFilterValues = Object.values(BuildJobStatusFilter);
Expand Down Expand Up @@ -429,30 +428,39 @@ export class BuildQueueComponent implements OnInit, OnDestroy {
* @param buildJobId The id of the build job
*/
viewBuildLogs(modal: any, buildJobId: string | undefined): void {
this.rawBuildLogsString = '';
this.displayedBuildJobId = undefined;
if (buildJobId) {
this.openModal(modal, true);
this.displayedBuildJobId = buildJobId;
this.buildQueueService.getBuildJobLogs(buildJobId).subscribe({
next: (buildLogs: BuildLogEntry[]) => {
this.rawBuildLogs = buildLogs.map((entry) => {
const logLines = entry.log ? entry.log.split('\n') : [];
return { time: entry.time, logLines: logLines };
});
next: (buildLogs: string) => {
this.rawBuildLogsString = buildLogs;
},
error: (res: HttpErrorResponse) => {
onError(this.alertService, res, false);
},
});
}
}

/**
* Download the build logs of a specific build job
*/
downloadBuildLogs(): void {
if (this.displayedBuildJobId) {
const url = `/api/build-log/${this.displayedBuildJobId}`;
window.open(url, '_blank');
if (this.displayedBuildJobId && this.rawBuildLogsString) {
const blob = new Blob([this.rawBuildLogsString], { type: 'text/plain' });
BBesrour marked this conversation as resolved.
Show resolved Hide resolved

const url = window.URL.createObjectURL(blob);

const anchor = document.createElement('a');
anchor.href = url;
anchor.download = `${this.displayedBuildJobId}.log`;

// Programmatically click the link to trigger download
anchor.click();

// Clean up the URL object
window.URL.revokeObjectURL(url);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { BuildJob, BuildJobStatistics, FinishedBuildJob, SpanType } from 'app/en
import { createNestedRequestOption } from 'app/shared/util/request.util';
import { HttpResponse } from '@angular/common/http';
import { FinishedBuildJobFilter } from 'app/localci/build-queue/build-queue.component';
import { BuildLogEntry } from 'app/entities/programming/build-log.model';

@Injectable({ providedIn: 'root' })
export class BuildQueueService {
Expand Down Expand Up @@ -195,8 +194,8 @@ export class BuildQueueService {
* Get all build jobs of a course in the queue
* @param buildJobId
*/
getBuildJobLogs(buildJobId: string): Observable<BuildLogEntry[]> {
return this.http.get<BuildLogEntry[]>(`${this.resourceUrl}/build-log/${buildJobId}/entries`).pipe(
getBuildJobLogs(buildJobId: string): Observable<string> {
return this.http.get(`${this.resourceUrl}/build-log/${buildJobId}`, { responseType: 'text' }).pipe(
catchError(() => {
return throwError(() => new Error('artemisApp.buildQueue.logs.errorFetchingLogs'));
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;

import org.junit.jupiter.api.AfterEach;
Expand Down Expand Up @@ -347,30 +346,6 @@ void testGetBuildLogsForResult() throws Exception {
}
}

@Test
@WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR")
void testGetBuildLogsEntriesForResult() throws Exception {
try {
buildJobRepository.save(finishedJobForLogs);
BuildLogDTO buildLogEntry = new BuildLogDTO(ZonedDateTime.now(), "Dummy log");
buildLogEntryService.saveBuildLogsToFile(List.of(buildLogEntry), "6", programmingExercise);
var response = request.get("/api/build-log/6/entries", HttpStatus.OK, List.class);

LinkedHashMap<?, ?> responseMap = ((LinkedHashMap<?, ?>) response.getFirst());
String log = responseMap.get("log").toString();
ZonedDateTime time = ZonedDateTime.parse(responseMap.get("time").toString());
assertThat(response).hasSize(1);
assertThat(buildLogEntry.log()).isEqualTo(log);
assertThat(buildLogEntry.time()).isEqualTo(time);

}
finally {
Path buildLogFile = Path.of("build-logs").resolve(programmingExercise.getCourseViaExerciseGroupOrCourseMember().getShortName())
.resolve(programmingExercise.getShortName()).resolve("6.log");
Files.deleteIfExists(buildLogFile);
}
}

@Test
@WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN")
void testGetBuildJobStatistics() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { HttpResponse } from '@angular/common/http';
import { SortingOrder } from 'app/shared/table/pageable-table';
import { LocalStorageService } from 'ngx-webstorage';
import { MockLocalStorageService } from '../../../helpers/mocks/service/mock-local-storage.service';
import { BuildLogEntry, BuildLogLines } from '../../../../../../main/webapp/app/entities/programming/build-log.model';
import { MockNgbModalService } from '../../../helpers/mocks/service/mock-ngb-modal.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';

Expand Down Expand Up @@ -265,17 +264,6 @@ describe('BuildQueueComponent', () => {
numberOfAppliedFilters: 0,
};

const buildLogEntries: BuildLogEntry[] = [
{
time: dayjs('2024-01-01'),
log: 'log1',
},
{
time: dayjs('2024-01-02'),
log: 'log2',
},
];

beforeEach(waitForAsync(() => {
mockActivatedRoute = { params: of({ courseId: testCourseId }) };

Expand Down Expand Up @@ -386,7 +374,7 @@ describe('BuildQueueComponent', () => {
component.ngOnInit();
component.onTabChange(SpanType.WEEK);

expect(mockBuildQueueService.getBuildJobStatistics).toHaveBeenCalledOnce();
expect(mockBuildQueueService.getBuildJobStatistics).toHaveBeenCalled();
expect(component.buildJobStatistics).toEqual(mockBuildJobStatistics);
});

Expand Down Expand Up @@ -673,21 +661,35 @@ describe('BuildQueueComponent', () => {

it('should download build logs', () => {
const buildJobId = '1';
jest.spyOn(window, 'open').mockImplementation();

mockBuildQueueService.getBuildJobLogs = jest.fn().mockReturnValue(of(buildLogEntries));

const buildLogsMultiLines: BuildLogLines[] = buildLogEntries.map((entry) => {
return { time: entry.time, logLines: entry.log.split('\n') };
});
const buildLogsMultiLines = 'log1\nlog2\nlog3';
mockBuildQueueService.getBuildJobLogs = jest.fn().mockReturnValue(of(buildLogsMultiLines));

component.viewBuildLogs(undefined, buildJobId);

expect(mockBuildQueueService.getBuildJobLogs).toHaveBeenCalledWith(buildJobId);
expect(component.rawBuildLogs).toEqual(buildLogsMultiLines);
expect(component.rawBuildLogsString).toEqual(buildLogsMultiLines);

const mockBlob = new Blob([buildLogsMultiLines], { type: 'text/plain' });
const mockUrl = 'blob:http://localhost:12345';
const mockAnchor = {
href: '',
download: '',
click: jest.fn(),
};

global.URL.createObjectURL = jest.fn(() => 'mockedURL');
global.URL.revokeObjectURL = jest.fn();
jest.spyOn(window.URL, 'createObjectURL').mockReturnValue(mockUrl);
jest.spyOn(document, 'createElement').mockReturnValue(mockAnchor as unknown as HTMLAnchorElement);
jest.spyOn(window.URL, 'revokeObjectURL').mockImplementation();

component.downloadBuildLogs();

expect(window.open).toHaveBeenCalledWith(`/api/build-log/${component.displayedBuildJobId}`, '_blank');
expect(window.URL.createObjectURL).toHaveBeenCalledWith(mockBlob);
expect(mockAnchor.href).toBe(mockUrl);
expect(mockAnchor.download).toBe(`${buildJobId}.log`);
expect(mockAnchor.click).toHaveBeenCalled();
BBesrour marked this conversation as resolved.
Show resolved Hide resolved
expect(window.URL.revokeObjectURL).toHaveBeenCalledWith(mockUrl);
});
});
Loading
Loading