diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2296ed26a829..22968f1531b4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -111,10 +111,10 @@ Prerequisites: - [ ] Test 2 ### Test Coverage - - - - + + + + + + + + + diff --git a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts index 711fa4a40dce..5e79ceac3c75 100644 --- a/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts +++ b/src/main/webapp/app/localci/build-agents/build-agent-summary/build-agent-summary.component.ts @@ -1,12 +1,14 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { BuildAgentInformation } from 'app/entities/programming/build-agent-information.model'; +import { BuildAgentInformation, BuildAgentStatus } from 'app/entities/programming/build-agent-information.model'; import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; import { Subscription } from 'rxjs'; -import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { faPause, faPlay, faTimes } from '@fortawesome/free-solid-svg-icons'; import { BuildQueueService } from 'app/localci/build-queue/build-queue.service'; import { Router } from '@angular/router'; import { BuildAgent } from 'app/entities/programming/build-agent.model'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { AlertService, AlertType } from 'app/core/util/alert.service'; @Component({ selector: 'jhi-build-agents', @@ -23,13 +25,17 @@ export class BuildAgentSummaryComponent implements OnInit, OnDestroy { routerLink: string; //icons - faTimes = faTimes; + protected readonly faTimes = faTimes; + protected readonly faPause = faPause; + protected readonly faPlay = faPlay; constructor( private websocketService: JhiWebsocketService, private buildAgentsService: BuildAgentsService, private buildQueueService: BuildQueueService, private router: Router, + private modalService: NgbModal, + private alertService: AlertService, ) {} ngOnInit() { @@ -59,7 +65,9 @@ export class BuildAgentSummaryComponent implements OnInit, OnDestroy { private updateBuildAgents(buildAgents: BuildAgentInformation[]) { this.buildAgents = buildAgents; - this.buildCapacity = this.buildAgents.reduce((sum, agent) => sum + (agent.maxNumberOfConcurrentBuildJobs || 0), 0); + this.buildCapacity = this.buildAgents + .filter((agent) => agent.status !== BuildAgentStatus.PAUSED) + .reduce((sum, agent) => sum + (agent.maxNumberOfConcurrentBuildJobs || 0), 0); this.currentBuilds = this.buildAgents.reduce((sum, agent) => sum + (agent.numberOfCurrentBuildJobs || 0), 0); } @@ -86,4 +94,47 @@ export class BuildAgentSummaryComponent implements OnInit, OnDestroy { this.buildQueueService.cancelAllRunningBuildJobsForAgent(buildAgentToCancel.buildAgent?.name).subscribe(); } } + + displayPauseBuildAgentModal(modal: any) { + this.modalService.open(modal); + } + + pauseAllBuildAgents(modal?: any) { + this.buildAgentsService.pauseAllBuildAgents().subscribe({ + next: () => { + this.load(); + this.alertService.addAlert({ + type: AlertType.SUCCESS, + message: 'artemisApp.buildAgents.alerts.buildAgentsPaused', + }); + }, + error: () => { + this.alertService.addAlert({ + type: AlertType.DANGER, + message: 'artemisApp.buildAgents.alerts.buildAgentPauseFailed', + }); + }, + }); + if (modal) { + modal.close(); + } + } + + resumeAllBuildAgents() { + this.buildAgentsService.resumeAllBuildAgents().subscribe({ + next: () => { + this.load(); + this.alertService.addAlert({ + type: AlertType.SUCCESS, + message: 'artemisApp.buildAgents.alerts.buildAgentsResumed', + }); + }, + error: () => { + this.alertService.addAlert({ + type: AlertType.DANGER, + message: 'artemisApp.buildAgents.alerts.buildAgentResumeFailed', + }); + }, + }); + } } diff --git a/src/main/webapp/app/localci/build-agents/build-agents.service.ts b/src/main/webapp/app/localci/build-agents/build-agents.service.ts index 6aac582940d3..c3b388586bcf 100644 --- a/src/main/webapp/app/localci/build-agents/build-agents.service.ts +++ b/src/main/webapp/app/localci/build-agents/build-agents.service.ts @@ -33,22 +33,44 @@ export class BuildAgentsService { */ pauseBuildAgent(agentName: string): Observable { const encodedAgentName = encodeURIComponent(agentName); - return this.http.put(`${this.adminResourceUrl}/agent/${encodedAgentName}/pause`, null).pipe( + return this.http.put(`${this.adminResourceUrl}/agents/${encodedAgentName}/pause`, null).pipe( catchError((err) => { return throwError(() => new Error(`Failed to pause build agent ${agentName}\n${err.message}`)); }), ); } + /** + * Pause All Build Agents + */ + pauseAllBuildAgents(): Observable { + return this.http.put(`${this.adminResourceUrl}/agents/pause-all`, null).pipe( + catchError((err) => { + return throwError(() => new Error(`Failed to pause build agents\n${err.message}`)); + }), + ); + } + /** * Resume Build Agent */ resumeBuildAgent(agentName: string): Observable { const encodedAgentName = encodeURIComponent(agentName); - return this.http.put(`${this.adminResourceUrl}/agent/${encodedAgentName}/resume`, null).pipe( + return this.http.put(`${this.adminResourceUrl}/agents/${encodedAgentName}/resume`, null).pipe( catchError((err) => { return throwError(() => new Error(`Failed to resume build agent ${agentName}\n${err.message}`)); }), ); } + + /** + * Resume all Build Agents + */ + resumeAllBuildAgents(): Observable { + return this.http.put(`${this.adminResourceUrl}/agents/resume-all`, null).pipe( + catchError((err) => { + return throwError(() => new Error(`Failed to resume build agents\n${err.message}`)); + }), + ); + } } diff --git a/src/main/webapp/app/shared/server-date.service.ts b/src/main/webapp/app/shared/server-date.service.ts index 23ee0c2cbc61..c13e9b126a45 100644 --- a/src/main/webapp/app/shared/server-date.service.ts +++ b/src/main/webapp/app/shared/server-date.service.ts @@ -36,7 +36,7 @@ export class ArtemisServerDateService implements ServerDateService { const now = dayjs(new Date()); if (this.recentClientDates.length > 4) { // only if some recent client dates (i.e. recent syncs) are older than 60s - shouldSync = this.recentClientDates.some((recentClientDate) => now.diff(recentClientDate, 's') > 60); + shouldSync = this.recentClientDates.some((recentClientDate) => Math.abs(now.diff(recentClientDate, 's')) > 60); } else { // definitely sync if we do not have 5 elements yet shouldSync = true; diff --git a/src/main/webapp/app/utils/date.utils.ts b/src/main/webapp/app/utils/date.utils.ts index d05d241f29f1..57ea6bfe6d25 100644 --- a/src/main/webapp/app/utils/date.utils.ts +++ b/src/main/webapp/app/utils/date.utils.ts @@ -75,6 +75,7 @@ export function dayOfWeekZeroSundayToZeroMonday(dayOfWeekZeroSunday: number): nu return (dayOfWeekZeroSunday + 6) % 7; } -export function isDateLessThanAWeekInTheFuture(date: dayjs.Dayjs): boolean { - return date.isBetween(dayjs().add(1, 'week'), dayjs()); +export function isDateLessThanAWeekInTheFuture(date: dayjs.Dayjs, compareTime?: dayjs.Dayjs): boolean { + const now = compareTime ?? dayjs(); + return date.isBetween(now.add(1, 'week'), now); } diff --git a/src/main/webapp/i18n/de/buildAgents.json b/src/main/webapp/i18n/de/buildAgents.json index 186bd1432540..bad9a78469d3 100644 --- a/src/main/webapp/i18n/de/buildAgents.json +++ b/src/main/webapp/i18n/de/buildAgents.json @@ -22,10 +22,15 @@ "buildAgentResumed": "Anfrage zum Fortsetzen des BuildJobs erfolgreich gesendet. Der Agent wird wieder neue BuildJobs annehmen.", "buildAgentPauseFailed": "Anhalten des Build-Agenten fehlgeschlagen.", "buildAgentResumeFailed": "Fortsetzen des Build-Agenten fehlgeschlagen.", - "buildAgentWithoutName": "Der Name des Build-Agenten ist erforderlich." + "buildAgentWithoutName": "Der Name des Build-Agenten ist erforderlich.", + "buildAgentsPaused": "Anfrage zum Anhalten aller Build-Agenten erfolgreich gesendet. Die Agenten akzeptieren keine neuen Build-Jobs und werden die aktuellen Build-Jobs entweder ordnungsgemäß beenden oder nach einer konfigurierbaren Anzahl von Sekunden abbrechen.", + "buildAgentsResumed": "Anfrage zum Fortsetzen aller Build-Agenten erfolgreich gesendet. Die Agenten werden wieder neue Build-Jobs annehmen." }, "pause": "Anhalten", - "resume": "Fortsetzen" + "resume": "Fortsetzen", + "pauseAll": "Alle Agenten Anhalten", + "resumeAll": "Alle Agenten fortsetzen", + "pauseAllWarning": "Du bist dabei, alle Build-Agenten anzuhalten. Dies wird verhindern, dass sie neue Build-Jobs verarbeiten.\nBist du sicher, dass du fortfahren möchtest?" } } } diff --git a/src/main/webapp/i18n/en/buildAgents.json b/src/main/webapp/i18n/en/buildAgents.json index 974086655128..ce0cf7620104 100644 --- a/src/main/webapp/i18n/en/buildAgents.json +++ b/src/main/webapp/i18n/en/buildAgents.json @@ -20,12 +20,17 @@ "alerts": { "buildAgentPaused": "Build agent pause request sent successfully. The agent will not accept new build jobs and will gracefully finish the current build jobs or will cancel them after a configurable seconds.", "buildAgentResumed": "Build agent resume request sent successfully. The agent will start accepting new build jobs.", - "buildAgentPauseFailed": "Failed to pause the build agent.", - "buildAgentResumeFailed": "Failed to resume the build agent.", - "buildAgentWithoutName": "Build agent name is required." + "buildAgentPauseFailed": "Failed to pause build agent.", + "buildAgentResumeFailed": "Failed to resume build agent.", + "buildAgentWithoutName": "Build agent name is required.", + "buildAgentsPaused": "Pause request sent to all build agents. The agents will not accept new build jobs and will gracefully finish the current build jobs or will cancel them after a configurable seconds.", + "buildAgentsResumed": "Resume request sent to all build agents. The agents will start accepting new build jobs." }, "pause": "Pause", - "resume": "Resume" + "resume": "Resume", + "pauseAll": "Pause All Agents", + "resumeAll": "Resume All Agents", + "pauseAllWarning": "You are about to pause all build agents. This will prevent them from processing any new build jobs.\nAre you sure you want to proceed?" } } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/StaticCodeAnalysisParserUnitTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/StaticCodeAnalysisParserUnitTest.java index 2e8886ec0fbc..946900357324 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/StaticCodeAnalysisParserUnitTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/StaticCodeAnalysisParserUnitTest.java @@ -2,21 +2,20 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.fail; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; -import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.stream.Collectors; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisReportDTO; import de.tum.cit.aet.artemis.programming.service.localci.scaparser.ReportParser; -import de.tum.cit.aet.artemis.programming.service.localci.scaparser.exception.ParserException; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.exception.UnsupportedToolException; /** * Tests each parser with an example file @@ -27,92 +26,61 @@ class StaticCodeAnalysisParserUnitTest { private static final Path REPORTS_FOLDER_PATH = Paths.get("src", "test", "resources", "test-data", "static-code-analysis", "reports"); + private final ObjectMapper mapper = new ObjectMapper(); + + private void testParserWithFile(String toolGeneratedReportFileName, String expectedJSONReportFileName) throws IOException { + testParserWithFileNamed(toolGeneratedReportFileName, toolGeneratedReportFileName, expectedJSONReportFileName); + } + /** * Compares the parsed JSON report with the expected JSON report * * @param toolGeneratedReportFileName The name of the file contains the report as generated by the different tools * @param expectedJSONReportFileName The name of the file that contains the parsed report - * @throws ParserException If an exception occurs that is not already handled by the parser itself, e.g. caused by the json-parsing */ - private void testParserWithFile(String toolGeneratedReportFileName, String expectedJSONReportFileName) throws ParserException, IOException { - File toolReport = REPORTS_FOLDER_PATH.resolve(toolGeneratedReportFileName).toFile(); + private void testParserWithFileNamed(String toolGeneratedReportFileName, String fileName, String expectedJSONReportFileName) throws IOException { + Path actualReportPath = REPORTS_FOLDER_PATH.resolve(toolGeneratedReportFileName); + File expectedJSONReportFile = EXPECTED_FOLDER_PATH.resolve(expectedJSONReportFileName).toFile(); - ReportParser parser = new ReportParser(); - String actual = parser.transformToJSONReport(toolReport); - - try (BufferedReader reader = Files.newBufferedReader(EXPECTED_FOLDER_PATH.resolve(expectedJSONReportFileName))) { - String expected = reader.lines().collect(Collectors.joining(System.lineSeparator())); - assertThat(actual).isEqualTo(expected); - } + String actualReportContent = Files.readString(actualReportPath); + testParserWithContent(actualReportContent, fileName, expectedJSONReportFile); } - private void testParserWithNullValue() throws ParserException { - ReportParser parser = new ReportParser(); - parser.transformToJSONReport(null); + private void testParserWithContent(String actualReportContent, String actualReportFilename, File expectedJSONReportFile) throws IOException { + StaticCodeAnalysisReportDTO actualReport = ReportParser.getReport(actualReportContent, actualReportFilename); + + StaticCodeAnalysisReportDTO expectedReport = mapper.readValue(expectedJSONReportFile, StaticCodeAnalysisReportDTO.class); + + assertThat(actualReport).isEqualTo(expectedReport); } @Test void testCheckstyleParser() throws IOException { - try { - testParserWithFile("checkstyle-result.xml", "checkstyle.txt"); - } - catch (ParserException e) { - fail("Checkstyle parser failed with exception: " + e.getMessage()); - } + testParserWithFile("checkstyle-result.xml", "checkstyle.txt"); } @Test void testPMDCPDParser() throws IOException { - try { - testParserWithFile("cpd.xml", "pmd_cpd.txt"); - } - catch (ParserException e) { - fail("PMD-CPD parser failed with exception: " + e.getMessage()); - } + testParserWithFile("cpd.xml", "pmd_cpd.txt"); } @Test void testPMDParser() throws IOException { - try { - testParserWithFile("pmd.xml", "pmd.txt"); - } - catch (ParserException e) { - fail("PMD parser failed with exception: " + e.getMessage()); - } + testParserWithFile("pmd.xml", "pmd.txt"); } @Test void testSpotbugsParser() throws IOException { - try { - testParserWithFile("spotbugsXml.xml", "spotbugs.txt"); - } - catch (ParserException e) { - fail("Spotbugs parser failed with exception: " + e.getMessage()); - } + testParserWithFile("spotbugsXml.xml", "spotbugs.txt"); } @Test - void testParseInvalidFilename() { - assertThatCode(() -> testParserWithFile("cpd_invalid.txt", "invalid_filename.txt")).isInstanceOf(ParserException.class); - } - - @Test - void testParseInvalidXML() throws Exception { - testParserWithFile("invalid_xml.xml", "invalid_xml.txt"); + void testParseInvalidXML() throws IOException { + assertThatCode(() -> testParserWithFileNamed("invalid_xml.xml", "pmd.xml", "invalid_xml.txt")).isInstanceOf(RuntimeException.class); } @Test void testInvalidName() throws IOException { - try { - testParserWithFile("invalid_name.xml", "invalid_name.txt"); - } - catch (ParserException e) { - fail("Parser failed with exception: " + e.getMessage()); - } - } - - @Test - void testThrowsParserException() { - assertThatExceptionOfType(ParserException.class).isThrownBy(this::testParserWithNullValue); + assertThatCode(() -> testParserWithFile("invalid_name.xml", "invalid_name.txt")).isInstanceOf(UnsupportedToolException.class); } } 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 28a919289e61..9d02a9f4dcaf 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 @@ -364,10 +364,29 @@ void testPauseBuildAgent() throws Exception { // We need to clear the processing jobs to avoid the agent being set to ACTIVE again processingJobs.clear(); - request.put("/api/admin/agent/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/pause", null, HttpStatus.NO_CONTENT); + request.put("/api/admin/agents/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/pause", null, HttpStatus.NO_CONTENT); await().until(() -> buildAgentInformation.get(agent1.buildAgent().memberAddress()).status() == BuildAgentInformation.BuildAgentStatus.PAUSED); - request.put("/api/admin/agent/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/resume", null, HttpStatus.NO_CONTENT); + request.put("/api/admin/agents/" + URLEncoder.encode(agent1.buildAgent().name(), StandardCharsets.UTF_8) + "/resume", null, HttpStatus.NO_CONTENT); await().until(() -> buildAgentInformation.get(agent1.buildAgent().memberAddress()).status() == BuildAgentInformation.BuildAgentStatus.IDLE); } + + @Test + @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") + void testPauseAllBuildAgents() throws Exception { + // We need to clear the processing jobs to avoid the agent being set to ACTIVE again + processingJobs.clear(); + + request.put("/api/admin/agents/pause-all", null, HttpStatus.NO_CONTENT); + await().until(() -> { + var agents = buildAgentInformation.values(); + return agents.stream().allMatch(agent -> agent.status() == BuildAgentInformation.BuildAgentStatus.PAUSED); + }); + + request.put("/api/admin/agents/resume-all", null, HttpStatus.NO_CONTENT); + await().until(() -> { + var agents = buildAgentInformation.values(); + return agents.stream().allMatch(agent -> agent.status() == BuildAgentInformation.BuildAgentStatus.IDLE); + }); + } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/SarifParserTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/SarifParserTest.java new file mode 100644 index 000000000000..a84df20165d7 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/SarifParserTest.java @@ -0,0 +1,476 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.strategy.sarif; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import de.tum.cit.aet.artemis.programming.domain.StaticCodeAnalysisTool; +import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisIssue; +import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisReportDTO; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.ReportingDescriptor; + +class SarifParserTest { + + static class FullDescriptionCategorizer implements RuleCategorizer { + + @Override + public String categorizeRule(ReportingDescriptor rule) { + return rule.getOptionalFullDescription().orElseThrow().text(); + } + } + + @Test + void testEmpty() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": { + "rules": [] + } + }, + "results": [] + } + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new IdCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).isEmpty(); + } + + @Test + void testMetadata() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": { + "rules": [ + { + "fullDescription": { + "text": "CATEGORY" + }, + "id": "RULE_ID" + } + ] + } + }, + "results": [ + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 10, + "startColumn": 20, + "endLine": 30, + "endColumn": 40 + } + } + } + ], + "message": { + "text": "MESSAGE" + }, + "ruleId": "RULE_ID", + "ruleIndex": 0 + } + ] + } + ] + } + """; + StaticCodeAnalysisIssue expected = new StaticCodeAnalysisIssue("/path/to/file.txt", 10, 30, 20, 40, "RULE_ID", "CATEGORY", "MESSAGE", "error", null); + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new FullDescriptionCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).singleElement().isEqualTo(expected); + } + + @Test + void testMessageLookup() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": { + "rules": [ + { + "fullDescription": { + "text": "FULL_DESCRIPTION" + }, + "id": "A001", + "messageStrings": { + "MESSAGE_ID_A": { + "text": "RULE_MESSAGE_CONTENT_A" + } + } + } + ], + "globalMessageStrings": { + "MESSAGE_ID_A": { + "text": "GLOBAL_MESSAGE_CONTENT_A" + }, + "MESSAGE_ID_B": { + "text": "GLOBAL_MESSAGE_CONTENT_B" + } + } + } + }, + "results": [ + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "id": "MESSAGE_ID_A" + }, + "ruleId": "A001", + "ruleIndex": 0 + }, + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "id": "MESSAGE_ID_B" + }, + "ruleId": "B001" + } + ] + } + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new IdCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).anyMatch(issue -> issue.rule().equals("A001") && issue.message().equals("RULE_MESSAGE_CONTENT_A")) + .anyMatch(issue -> issue.rule().equals("B001") && issue.message().equals("GLOBAL_MESSAGE_CONTENT_B")); + } + + @Test + void testHierarchicalRuleIdLookup() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": { + "rules": [ + { + "fullDescription": { + "text": "FULL_DESCRIPTION" + }, + "id": "A123" + } + ] + } + }, + "results": [ + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "text": "MESSAGE" + }, + "ruleId": "A123/subrule" + } + ] + } + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new FullDescriptionCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).singleElement().matches(issue -> issue.category().equals("FULL_DESCRIPTION")); + } + + @Test + void testRuleIndexLookup() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": { + "rules": [ + { + "fullDescription": { + "text": "FULL_DESCRIPTION_A" + }, + "id": "RULE_ID" + }, + { + "fullDescription": { + "text": "FULL_DESCRIPTION_B" + }, + "id": "RULE_ID" + } + ] + } + }, + "results": [ + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "text": "MESSAGE" + }, + "ruleId": "RULE_ID", + "ruleIndex": 1 + } + ] + } + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new FullDescriptionCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).singleElement().matches(issue -> issue.category().equals("FULL_DESCRIPTION_B")); + } + + @Test + void testInvalidJSON() { + String report = """ + { + "runs": [ + { + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new IdCategorizer()); + assertThatThrownBy(() -> parser.parse(report)).hasCauseInstanceOf(JsonProcessingException.class); + } + + @Test + void testFilterMalformedSarif() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": {} + }, + "results": [ + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "text": "VALID" + }, + "ruleId": "A001" + }, + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + } + } + } + ], + "message": { + "text": "REGION MISSING" + }, + "ruleId": "A002" + }, + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "text": "NO_RULE_ID" + } + }, + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "id": "INVALID_MESSAGE_ID" + }, + "ruleId": "A004" + } + ] + } + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new IdCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).singleElement().matches(issue -> issue.rule().equals("A001")); + } + + @Test + void testFilterInformationMissing() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": {} + }, + "results": [ + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "text": "VALID" + }, + "ruleId": "A001" + }, + { + "message": { + "text": "LOCATION MISSING" + }, + "ruleId": "A002" + }, + { + "locations": [ + { + "physicalLocation": { + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "text": "PATH MISSING" + }, + "ruleId": "A003" + }, + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "byteOffset": 0, + "byteLength": 10 + } + } + } + ], + "message": { + "text": "NOT A TEXT REGION" + }, + "ruleId": "A004" + } + ] + } + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new IdCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).singleElement().matches(issue -> issue.rule().equals("A001")); + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseFactory.java b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseFactory.java index e4dca9d8be57..bf4c207df1e6 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseFactory.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseFactory.java @@ -341,6 +341,7 @@ private static StaticCodeAnalysisIssue generateStaticCodeAnalysisIssue(StaticCod case PMD_CPD -> "Copy/Paste Detection"; case SWIFTLINT -> "swiftLint"; // TODO: rene: set better value after categories are better defined case GCC -> "Memory"; + case OTHER -> "Other"; }; return new StaticCodeAnalysisIssue(Constants.STUDENT_WORKING_DIRECTORY + "/www/packagename/Class1.java", // filePath diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts index fe898f69474a..f71ad2c8d920 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agent-summary.component.spec.ts @@ -2,18 +2,19 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { BuildAgentSummaryComponent } from 'app/localci/build-agents/build-agent-summary/build-agent-summary.component'; import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; import { BuildAgentsService } from 'app/localci/build-agents/build-agents.service'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; import { BuildJob } from 'app/entities/programming/build-job.model'; import dayjs from 'dayjs/esm'; import { ArtemisTestModule } from '../../../test.module'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { DataTableComponent } from 'app/shared/data-table/data-table.component'; -import { MockComponent, MockPipe } from 'ng-mocks'; +import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; import { NgxDatatableModule } from '@siemens/ngx-datatable'; import { BuildAgentInformation, BuildAgentStatus } from '../../../../../../main/webapp/app/entities/programming/build-agent-information.model'; import { RepositoryInfo, TriggeredByPushTo } from 'app/entities/programming/repository-info.model'; import { JobTimingInfo } from 'app/entities/job-timing-info.model'; import { BuildConfig } from 'app/entities/programming/build-config.model'; +import { AlertService, AlertType } from '../../../../../../main/webapp/app/core/util/alert.service'; describe('BuildAgentSummaryComponent', () => { let component: BuildAgentSummaryComponent; @@ -27,6 +28,8 @@ describe('BuildAgentSummaryComponent', () => { const mockBuildAgentsService = { getBuildAgentSummary: jest.fn().mockReturnValue(of([])), + pauseAllBuildAgents: jest.fn().mockReturnValue(of({})), + resumeAllBuildAgents: jest.fn().mockReturnValue(of({})), }; const repositoryInfo: RepositoryInfo = { @@ -135,6 +138,8 @@ describe('BuildAgentSummaryComponent', () => { status: BuildAgentStatus.ACTIVE, }, ]; + let alertService: AlertService; + let alertServiceAddAlertStub: jest.SpyInstance; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -144,11 +149,14 @@ describe('BuildAgentSummaryComponent', () => { { provide: JhiWebsocketService, useValue: mockWebsocketService }, { provide: BuildAgentsService, useValue: mockBuildAgentsService }, { provide: DataTableComponent, useClass: DataTableComponent }, + MockProvider(AlertService), ], }).compileComponents(); fixture = TestBed.createComponent(BuildAgentSummaryComponent); component = fixture.componentInstance; + alertService = TestBed.inject(AlertService); + alertServiceAddAlertStub = jest.spyOn(alertService, 'addAlert'); })); beforeEach(() => { @@ -218,4 +226,36 @@ describe('BuildAgentSummaryComponent', () => { expect(component.buildCapacity).toBe(0); expect(component.currentBuilds).toBe(0); }); + + it('should call correct service method when pausing and resuming build agents', () => { + component.pauseAllBuildAgents(); + expect(alertServiceAddAlertStub).toHaveBeenCalledWith({ + type: AlertType.SUCCESS, + message: 'artemisApp.buildAgents.alerts.buildAgentsPaused', + }); + + component.resumeAllBuildAgents(); + expect(alertServiceAddAlertStub).toHaveBeenCalledWith({ + type: AlertType.SUCCESS, + message: 'artemisApp.buildAgents.alerts.buildAgentsResumed', + }); + }); + + it('should show alert when error in pausing or resuming build agents', () => { + mockBuildAgentsService.pauseAllBuildAgents.mockReturnValue(throwError(() => new Error())); + + component.pauseAllBuildAgents(); + expect(alertServiceAddAlertStub).toHaveBeenCalledWith({ + type: AlertType.DANGER, + message: 'artemisApp.buildAgents.alerts.buildAgentPauseFailed', + }); + + mockBuildAgentsService.resumeAllBuildAgents.mockReturnValue(throwError(() => new Error())); + + component.resumeAllBuildAgents(); + expect(alertServiceAddAlertStub).toHaveBeenCalledWith({ + type: AlertType.DANGER, + message: 'artemisApp.buildAgents.alerts.buildAgentResumeFailed', + }); + }); }); diff --git a/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts b/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts index 261acf610ff2..18b9f0135b83 100644 --- a/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts +++ b/src/test/javascript/spec/component/localci/build-agents/build-agents.service.spec.ts @@ -134,7 +134,7 @@ describe('BuildAgentsService', () => { it('should pause build agent', () => { service.pauseBuildAgent('buildAgent1').subscribe(); - const req = httpMock.expectOne(`${service.adminResourceUrl}/agent/buildAgent1/pause`); + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/buildAgent1/pause`); expect(req.request.method).toBe('PUT'); req.flush({}); }); @@ -142,7 +142,23 @@ describe('BuildAgentsService', () => { it('should resume build agent', () => { service.resumeBuildAgent('buildAgent1').subscribe(); - const req = httpMock.expectOne(`${service.adminResourceUrl}/agent/buildAgent1/resume`); + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/buildAgent1/resume`); + expect(req.request.method).toBe('PUT'); + req.flush({}); + }); + + it('should pause all build agents', () => { + service.pauseAllBuildAgents().subscribe(); + + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/pause-all`); + expect(req.request.method).toBe('PUT'); + req.flush({}); + }); + + it('should resume all build agents', () => { + service.resumeAllBuildAgents().subscribe(); + + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/resume-all`); expect(req.request.method).toBe('PUT'); req.flush({}); }); @@ -152,7 +168,7 @@ describe('BuildAgentsService', () => { const observable = lastValueFrom(service.pauseBuildAgent('buildAgent1')); - const req = httpMock.expectOne(`${service.adminResourceUrl}/agent/buildAgent1/pause`); + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/buildAgent1/pause`); expect(req.request.method).toBe('PUT'); req.flush({ message: errorMessage }, { status: 500, statusText: 'Internal Server Error' }); @@ -170,7 +186,42 @@ describe('BuildAgentsService', () => { const observable = lastValueFrom(service.resumeBuildAgent('buildAgent1')); // Set up the expected HTTP request and flush the response with an error. - const req = httpMock.expectOne(`${service.adminResourceUrl}/agent/buildAgent1/resume`); + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/buildAgent1/resume`); + expect(req.request.method).toBe('PUT'); + req.flush({ message: errorMessage }, { status: 500, statusText: 'Internal Server Error' }); + + try { + await observable; + throw new Error('expected an error, but got a success'); + } catch (error) { + expect(error.message).toContain(errorMessage); + } + }); + + it('should handle pause all build agents error', async () => { + const errorMessage = 'Failed to pause build agents'; + + const observable = lastValueFrom(service.pauseAllBuildAgents()); + + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/pause-all`); + expect(req.request.method).toBe('PUT'); + req.flush({ message: errorMessage }, { status: 500, statusText: 'Internal Server Error' }); + + try { + await observable; + throw new Error('expected an error, but got a success'); + } catch (error) { + expect(error.message).toContain(errorMessage); + } + }); + + it('should handle resume all build agents error', async () => { + const errorMessage = 'Failed to resume build agents'; + + const observable = lastValueFrom(service.resumeAllBuildAgents()); + + // Set up the expected HTTP request and flush the response with an error. + const req = httpMock.expectOne(`${service.adminResourceUrl}/agents/resume-all`); expect(req.request.method).toBe('PUT'); req.flush({ message: errorMessage }, { status: 500, statusText: 'Internal Server Error' }); diff --git a/src/test/playwright/e2e/exam/ExamAssessment.spec.ts b/src/test/playwright/e2e/exam/ExamAssessment.spec.ts index ea6e00a07aea..4881c7337718 100644 --- a/src/test/playwright/e2e/exam/ExamAssessment.spec.ts +++ b/src/test/playwright/e2e/exam/ExamAssessment.spec.ts @@ -55,9 +55,6 @@ test.beforeAll('Create course', async ({ browser }) => { test.describe('Exam assessment', () => { test.describe.configure({ mode: 'serial' }); - let programmingAssessmentSuccessful = false; - let modelingAssessmentSuccessful = false; - let textAssessmentSuccessful = false; test.describe.serial('Programming exercise assessment', { tag: '@sequential' }, () => { test.beforeAll('Prepare exam', async ({ browser }) => { @@ -76,13 +73,10 @@ test.describe('Exam assessment', () => { await examAssessment.submit(); await login(studentOne, `/courses/${course.id}/exams/${exam.id}`); await examParticipation.checkResultScore('66.2%'); - programmingAssessmentSuccessful = true; }); test('Complaints about programming exercises assessment', async ({ examAssessment, page, studentAssessment, examManagement, courseAssessment, exerciseAssessment }) => { - if (programmingAssessmentSuccessful) { - await handleComplaint(course, exam, false, ExerciseType.PROGRAMMING, page, studentAssessment, examManagement, examAssessment, courseAssessment, exerciseAssessment); - } + await handleComplaint(course, exam, false, ExerciseType.PROGRAMMING, page, studentAssessment, examManagement, examAssessment, courseAssessment, exerciseAssessment); }); }); @@ -117,21 +111,18 @@ test.describe('Exam assessment', () => { await login(studentOne, `/courses/${course.id}/exams/${exam.id}`); await examParticipation.checkResultScore('40%'); - modelingAssessmentSuccessful = true; }); test('Complaints about modeling exercises assessment', async ({ examAssessment, page, studentAssessment, examManagement, courseAssessment, exerciseAssessment }) => { - if (modelingAssessmentSuccessful) { - await handleComplaint(course, exam, true, ExerciseType.MODELING, page, studentAssessment, examManagement, examAssessment, courseAssessment, exerciseAssessment); - } + await handleComplaint(course, exam, true, ExerciseType.MODELING, page, studentAssessment, examManagement, examAssessment, courseAssessment, exerciseAssessment); }); }); test.describe.serial('Text exercise assessment', { tag: '@slow' }, () => { test.beforeAll('Prepare exam', async ({ browser }) => { - examEnd = dayjs().add(40, 'seconds'); + examEnd = dayjs().add(20, 'seconds'); const page = await newBrowserPage(browser); - await prepareExam(course, examEnd, ExerciseType.TEXT, page); + await prepareExam(course, examEnd, ExerciseType.TEXT, page, 2); }); test('Assess a text exercise submission', async ({ login, examManagement, examAssessment, examParticipation, courseAssessment, exerciseAssessment }) => { @@ -144,13 +135,20 @@ test.describe('Exam assessment', () => { expect(response.status()).toBe(200); await login(studentOne, `/courses/${course.id}/exams/${exam.id}`); await examParticipation.checkResultScore('70%'); - textAssessmentSuccessful = true; + }); + + test('Instructor makes a second round of assessment', async ({ login, examManagement, examAssessment, examParticipation, courseAssessment, exerciseAssessment }) => { + await login(instructor); + await startAssessing(course.id!, exam.id!, 60000, examManagement, courseAssessment, exerciseAssessment, true, true); + await examAssessment.fillFeedback(9, 'Great job'); + const response = await examAssessment.submitTextAssessment(); + expect(response.status()).toBe(200); + await login(studentOne, `/courses/${course.id}/exams/${exam.id}`); + await examParticipation.checkResultScore('90%'); }); test('Complaints about text exercises assessment', async ({ examAssessment, page, studentAssessment, examManagement, courseAssessment, exerciseAssessment }) => { - if (textAssessmentSuccessful) { - await handleComplaint(course, exam, false, ExerciseType.TEXT, page, studentAssessment, examManagement, examAssessment, courseAssessment, exerciseAssessment); - } + await handleComplaint(course, exam, true, ExerciseType.TEXT, page, studentAssessment, examManagement, examAssessment, courseAssessment, exerciseAssessment, false); }); }); @@ -296,7 +294,7 @@ test.afterAll('Delete course', async ({ browser }) => { await courseManagementAPIRequests.deleteCourse(course, admin); }); -export async function prepareExam(course: Course, end: dayjs.Dayjs, exerciseType: ExerciseType, page: Page): Promise { +export async function prepareExam(course: Course, end: dayjs.Dayjs, exerciseType: ExerciseType, page: Page, numberOfCorrectionRounds: number = 1): Promise { const examAPIRequests = new ExamAPIRequests(page); const exerciseAPIRequests = new ExerciseAPIRequests(page); const examExerciseGroupCreation = new ExamExerciseGroupCreationPage(page, examAPIRequests, exerciseAPIRequests); @@ -326,7 +324,7 @@ export async function prepareExam(course: Course, end: dayjs.Dayjs, exerciseType course, startDate: dayjs(), endDate: end, - numberOfCorrectionRoundsInExam: 1, + numberOfCorrectionRoundsInExam: numberOfCorrectionRounds, examStudentReviewStart: resultDate, examStudentReviewEnd: resultDate.add(1, 'minute'), publishResultsDate: resultDate, @@ -379,10 +377,17 @@ async function startAssessing( examManagement: ExamManagementPage, courseAssessment: CourseAssessmentDashboardPage, exerciseAssessment: ExerciseAssessmentDashboardPage, + toggleSecondRound: boolean = false, + isFirstTimeAssessing: boolean = true, ) { await examManagement.openAssessmentDashboard(courseID, examID, timeout); await courseAssessment.clickExerciseDashboardButton(); - await exerciseAssessment.clickHaveReadInstructionsButton(); + if (toggleSecondRound) { + await exerciseAssessment.toggleSecondCorrectionRound(); + } + if (isFirstTimeAssessing) { + await exerciseAssessment.clickHaveReadInstructionsButton(); + } await exerciseAssessment.clickStartNewAssessment(); exerciseAssessment.getLockedMessage(); } @@ -398,6 +403,7 @@ async function handleComplaint( examAssessment: ExamAssessmentPage, courseAssessment: CourseAssessmentDashboardPage, exerciseAssessment: ExerciseAssessmentDashboardPage, + isFirstTimeAssessing: boolean = true, ) { const complaintText = 'Lorem ipsum dolor sit amet'; const complaintResponseText = ' consetetur sadipscing elitr'; @@ -411,8 +417,9 @@ async function handleComplaint( await Commands.login(page, instructor, `/course-management/${course.id}/exams`); await examManagement.openAssessmentDashboard(course.id!, exam.id!); await courseAssessment.clickExerciseDashboardButton(); - await exerciseAssessment.clickHaveReadInstructionsButton(); - + if (isFirstTimeAssessing) { + await exerciseAssessment.clickHaveReadInstructionsButton(); + } await exerciseAssessment.clickEvaluateComplaint(); await exerciseAssessment.checkComplaintText(complaintText); page.on('dialog', (dialog) => dialog.accept()); @@ -421,7 +428,7 @@ async function handleComplaint( } else { await examAssessment.acceptComplaint(complaintResponseText, true, exerciseType); } - if (exerciseType == ExerciseType.MODELING) { + if (exerciseType == ExerciseType.MODELING || reject) { await examAssessment.checkComplaintMessage('Response to complaint has been submitted'); } else { await examAssessment.checkComplaintMessage('The assessment was updated successfully.'); diff --git a/src/test/playwright/support/pageobjects/assessment/AbstractExerciseAssessmentPage.ts b/src/test/playwright/support/pageobjects/assessment/AbstractExerciseAssessmentPage.ts index 4a192240bf79..ba370c23cb34 100644 --- a/src/test/playwright/support/pageobjects/assessment/AbstractExerciseAssessmentPage.ts +++ b/src/test/playwright/support/pageobjects/assessment/AbstractExerciseAssessmentPage.ts @@ -13,6 +13,10 @@ export abstract class AbstractExerciseAssessmentPage { async addNewFeedback(points: number, feedback?: string) { await this.page.locator('.add-unreferenced-feedback').click(); + await this.fillFeedback(points, feedback); + } + + async fillFeedback(points: number, feedback?: string) { const unreferencedFeedback = this.page.locator('.unreferenced-feedback-detail'); await unreferencedFeedback.locator('#feedback-points').clear(); await unreferencedFeedback.locator('#feedback-points').fill(points.toString()); @@ -61,7 +65,11 @@ export abstract class AbstractExerciseAssessmentPage { responsePromise = this.page.waitForResponse(`${BASE_API}/programming-submissions/*/assessment-after-complaint`); break; case ExerciseType.TEXT: - responsePromise = this.page.waitForResponse(`${BASE_API}/participations/*/submissions/*/text-assessment-after-complaint`); + if (accept) { + responsePromise = this.page.waitForResponse(`${BASE_API}/participations/*/submissions/*/text-assessment-after-complaint`); + } else { + responsePromise = this.page.waitForResponse(`${BASE_API}/complaints/*/response`); + } break; case ExerciseType.MODELING: responsePromise = this.page.waitForResponse(`${BASE_API}/complaints/*/response`); diff --git a/src/test/playwright/support/pageobjects/assessment/ExerciseAssessmentDashboardPage.ts b/src/test/playwright/support/pageobjects/assessment/ExerciseAssessmentDashboardPage.ts index b501531c27f7..669abec6a3a1 100644 --- a/src/test/playwright/support/pageobjects/assessment/ExerciseAssessmentDashboardPage.ts +++ b/src/test/playwright/support/pageobjects/assessment/ExerciseAssessmentDashboardPage.ts @@ -13,8 +13,8 @@ export class ExerciseAssessmentDashboardPage { await this.page.click('#participate-in-assessment'); } - async clickStartNewAssessment() { - const startAssessingButton = this.page.locator('#start-new-assessment'); + async clickStartNewAssessment(assessmentRound: number = 1) { + const startAssessingButton = this.page.locator('#start-new-assessment').nth(assessmentRound - 1); await Commands.reloadUntilFound(this.page, startAssessingButton); await startAssessingButton.click(); } @@ -38,4 +38,8 @@ export class ExerciseAssessmentDashboardPage { async checkComplaintText(complaintText: string) { await expect(this.getComplaintText()).toHaveValue(complaintText); } + + async toggleSecondCorrectionRound() { + await this.page.getByTestId('toggle-second-correction').click(); + } } diff --git a/src/test/resources/test-data/static-code-analysis/reports/cpd_invalid.txt b/src/test/resources/test-data/static-code-analysis/reports/cpd_invalid.txt deleted file mode 100644 index 135463d9785e..000000000000 --- a/src/test/resources/test-data/static-code-analysis/reports/cpd_invalid.txt +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - createRandomDatesList() throws ParseException { - int listLength = randomIntegerWithin(RANDOM_FLOOR, RANDOM_CEILING); - List list = new ArrayList<>(); - - SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy"); - Date lowestDate = dateFormat.parse("08.11.2016"); - Date highestDate = dateFormat.parse("03.11.2020"); - - for (int i = 0; i < listLength; i++) { - Date randomDate = randomDateWithin(lowestDate, highestDate); - list.add(randomDate); - } - return list; - } - - private static List createRandomDatesList2() throws ParseException {]]> - - - - - - - diff --git a/supporting_scripts/generate_code_cov_table/README.md b/supporting_scripts/generate_code_cov_table/README.md index a5495c41fe53..067334a1615d 100644 --- a/supporting_scripts/generate_code_cov_table/README.md +++ b/supporting_scripts/generate_code_cov_table/README.md @@ -46,7 +46,7 @@ TOKEN=ab12cd Alternatively, you can use the command line argument `--token` to pass the credentials. ### Token -The token you must provide is a GitHub token with "Public Repository Access" checked. +The token you must provide is a [GitHub token](https://github.com/settings/tokens) with "Public Repository Access" checked. **Recommended for security, but not for convenience:** Don't store the `TOKEN` in the `.env` file, but let the script prompt you for it.