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.