diff --git a/pom.xml b/pom.xml
index 347604f69e91df65f64f45fe00664973a297cfec..52535481be75d91eb7361de157b7d9d0b8982dc4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -63,6 +63,17 @@
log4j-core
2.11.2
+
+ de.hftstuttgart
+ unified-ticketing
+ 0.1.1
+
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.9.0
+
@@ -74,4 +85,12 @@
+
+
+
+ unified-ticketing
+ https://transfer.hft-stuttgart.de/gitlab/api/v4/projects/154/packages/maven
+
+
+
diff --git a/src/main/java/de/hftstuttgart/rest/v1/task/TaskUpload.java b/src/main/java/de/hftstuttgart/rest/v1/task/TaskUpload.java
index 83d70b326fab2814bd4ba9e8c78152f89856956d..5f5a35cb9105402919b3283708ae66bdd3ba21fd 100644
--- a/src/main/java/de/hftstuttgart/rest/v1/task/TaskUpload.java
+++ b/src/main/java/de/hftstuttgart/rest/v1/task/TaskUpload.java
@@ -9,6 +9,7 @@ import de.hftstuttgart.rest.v1.unittest.UnitTestUpload;
import de.hftstuttgart.utils.DockerUtil;
import de.hftstuttgart.utils.FileUtil;
import de.hftstuttgart.utils.JGitUtil;
+import de.hftstuttgart.utils.UnifiedTicketingUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.core.env.Environment;
@@ -201,6 +202,9 @@ public class TaskUpload {
LOG.debug("convert to moodle understandable format");
LegacyMoodleResult moodleResult = LegacyMoodleResult.convertToModdleResult(resultSummary);
+ LOG.info("check for provided Ticketsystem information");
+ UnifiedTicketingUtil.reportResults(taskFileRef.getInputStream(), resultSummary);
+
LOG.info("submission tested successfully");
return moodleResult;
}
diff --git a/src/main/java/de/hftstuttgart/utils/UnifiedTicketingUtil.java b/src/main/java/de/hftstuttgart/utils/UnifiedTicketingUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..cbc2277f4d2aadf1b09f516e3d1c26a2a8f387d6
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/utils/UnifiedTicketingUtil.java
@@ -0,0 +1,285 @@
+package de.hftstuttgart.utils;
+
+import de.hftstuttgart.models.ModocotResult;
+import de.hftstuttgart.models.ModocotResultSummary;
+import de.hftstuttgart.unifiedticketing.core.Filter;
+import de.hftstuttgart.unifiedticketing.core.Ticket;
+import de.hftstuttgart.unifiedticketing.core.TicketBuilder;
+import de.hftstuttgart.unifiedticketing.core.TicketSystem;
+import de.hftstuttgart.unifiedticketing.exceptions.UnifiedticketingException;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class UnifiedTicketingUtil {
+
+ private final static Logger LOG = LogManager.getLogger(UnifiedTicketingUtil.class);
+
+ private final static String MODOCOT_LABEL = "MoDoCoT created";
+ private final static String MODOCOT_TITLE = " | " + MODOCOT_LABEL;
+
+ public static String createTicketDescriptionFromResult(TicketSystem ts, ModocotResult result, boolean compilationError) {
+ StringBuilder sb = new StringBuilder();
+
+ // heading
+ if (compilationError) {
+ sb.append(String.format("# %s is not compilable", result.name));
+ } else {
+ sb.append(String.format("# Unittest for %s fails", result.name));
+ }
+
+ sb.append("\n\n");
+
+ // meta data table
+ sb.append("|||\n");
+ sb.append("|:-|:-|\n");
+
+ if (compilationError) {
+ sb.append(String.format("|File|`%s`|\n", result.name));
+ } else {
+ sb.append(String.format("|Test|`%s`|\n", result.name));
+ }
+ sb.append(String.format("|Reason|`%s`|\n", result.failureReason));
+
+ if (compilationError) {
+ sb.append(String.format("|Line|`%s`|\n", result.lineNumber));
+ sb.append(String.format("|Column|`%s`|\n", result.columnNumber));
+ sb.append(String.format("|Position|`%s`|\n", result.position));
+ } else {
+ sb.append(String.format("|Type|`%s`|\n", result.failureType));
+ }
+
+ sb.append("\n\n");
+
+ // stacktrace
+ sb.append("\n\n");
+ sb.append("show stacktrace
\n\n");
+ sb.append("```");
+ sb.append(result.stacktrace);
+ sb.append("\n```");
+ sb.append("\n\n \n");
+
+ return sb.toString();
+ }
+
+ public static String createTicketTitleFromResult(TicketSystem ts, ModocotResult result, boolean compilationError) {
+ StringBuilder sb = new StringBuilder();
+ String separator = " | ";
+
+ if (compilationError) sb.append("compilation fails");
+ else sb.append("test method fails");
+ sb.append(separator);
+
+ sb.append(result.name);
+
+ if (!compilationError) {
+ sb.append(separator);
+ sb.append(result.failureReason);
+ }
+
+ // if label-support is not present, place global identifier into title
+ if (!ts.hasLabelSupport()) sb.append(MODOCOT_TITLE);
+
+ sb.append(separator);
+ sb.append(getHashForFailure(result));
+
+ return sb.toString();
+ }
+
+ public static Ticket createTicketFromResult(TicketSystem ts, ModocotResult result, boolean compilationError) {
+ TicketBuilder tb = ts.createTicket()
+ .title(createTicketTitleFromResult(ts, result, compilationError))
+ .description(createTicketDescriptionFromResult(ts, result, compilationError));
+
+ if (ts.hasLabelSupport()) tb.labels(Collections.singleton(MODOCOT_LABEL));
+
+ return tb.create();
+ }
+
+ public static Set fetchExistingModocotTickets(TicketSystem ts) {
+ Set ret = new HashSet<>();
+ Filter f = ts.find();
+
+ // depending on label support, identify MoDoCoT tickets by label or title containing string
+ if (ts.hasLabelSupport()) {
+ LOG.debug(String.format(
+ "ticketsystem has label support, using label %s to find modocot tickets", MODOCOT_LABEL));
+ f.withLabel(MODOCOT_LABEL);
+ }
+ else {
+ LOG.debug(String.format(
+ "ticketsystem without labels, searching for ticket titles containing %s", MODOCOT_TITLE));
+ f.withTitleContain(MODOCOT_TITLE);
+ }
+
+ LOG.debug("prepare pagination cycling");
+ // set first page and page size for pagination
+ int page = 1;
+ int pageSize = 10;
+ f.setPageSize(pageSize);
+ LOG.debug(String.format("using pagination with %s elements per page", pageSize));
+ // declare list for received tickets
+ List received;
+ // go into do-while to evaluate after receiving if another round is needed
+ do {
+ LOG.debug(String.format("calling page %s", page));
+ f.setPage(page++);
+ received = f.get();
+ ret.addAll(received);
+ } while (received.size() >= pageSize);
+
+ return ret;
+ }
+
+ public static String getHashForFailure(ModocotResult result) {
+ MessageDigest digest;
+
+ try {
+ digest = MessageDigest.getInstance("SHA-512");
+ } catch (NoSuchAlgorithmException e) {
+ throw new UnifiedticketingException(e);
+ }
+
+ byte[] data = digest.digest(String
+ .join(result.name, result.failureReason)
+ .getBytes(StandardCharsets.UTF_8));
+ StringBuilder hexString = new StringBuilder(2 * data.length);
+ for (byte character: data)
+ {
+ String hex = Integer.toHexString(0xff & character);
+ if (hex.length() == 1)
+ {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+
+ return hexString.substring(hexString.length() - 9, hexString.length() - 1);
+ }
+
+ public static void processResult(TicketSystem ts, Set tickets, ModocotResult result, boolean compilationError) {
+ LOG.debug(String.format("retrieving hash for %s", result.name));
+ String hash = getHashForFailure(result);
+
+ // check if corresponding ticket exists yet, otherwise create new one
+ Ticket ticket = tickets.stream()
+ .filter(t -> t.getTitle().endsWith(hash))
+ .findFirst()
+ .orElse(null);
+
+ if (ticket != null) {
+ // if yet existing, remove from found list
+ LOG.debug("found ticket with matching hash, removing from collection");
+ tickets.remove(ticket);
+ LOG.debug("updating ticket with new result");
+ updateTicketFromResult(ts, ticket, result, compilationError);
+ } else {
+ LOG.debug("no ticket found, creating new one");
+ createTicketFromResult(ts, result, compilationError);
+ }
+ }
+
+ /**
+ * search file for unified-ticketing URI's and report to every set ticket system,
+ * not waiting for it to finish and catching an eventually interrupted thread.
+ *
+ * @param meta student uploaded file
+ * @param resultSummary summary from the testrunner container
+ */
+ public static void reportResults(InputStream meta, ModocotResultSummary resultSummary) {
+ try {
+ reportResults(meta, resultSummary, false);
+ } catch (InterruptedException e) {
+ LOG.error(String.format("Unified-Ticketing got interrupted with: %s", e.getMessage()));
+ }
+ }
+
+ /**
+ * report all failures and compilation errors to all configured ticket systems.
+ * You can optionally wait for the ticket creation to finish, which is done in a separate thread.
+ *
+ * @param meta student uploaded file
+ * @param resultSummary summary from the testrunner container
+ * @param wait if we should block until the ticket creation has finished
+ * @throws InterruptedException
+ */
+ public static void reportResults(InputStream meta, ModocotResultSummary resultSummary, boolean wait) throws InterruptedException {
+
+ LOG.debug("preparing thread for ticket result submitting");
+ Thread unifiedTicketingUtil = new Thread(() -> {
+ // read student transmitted file
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(meta))) {
+ String line = null;
+ while ((line = br.readLine()) != null) {
+ TicketSystem ts = null;
+
+ // try each line as URI for a unified-ticketing instantiation
+ try {
+ ts = TicketSystem.fromUri(line);
+ LOG.info(String.format("ticket system for reporting found: %s", ts.baseUrl));
+ } catch (UnifiedticketingException e) {
+ // ignore if line didn't match a unified-ticketing URI
+ }
+
+ // if a ticketsystem got instantiated, start the submission.
+ // If errors occur, log it this time.
+ try {
+ if (ts != null) reportToTicketsystem(ts, resultSummary);
+ } catch (UnifiedticketingException e) {
+ LOG.warn(String.format(
+ "reporting fails to ticketsystem %s failed with: ",
+ ts.baseUrl,
+ e.getMessage()));
+ }
+ }
+ } catch (IOException e) {
+ LOG.error(String.format("couldn't read config to find lines for ticket reporting: %s", e.getMessage()));
+ }
+ });
+
+ LOG.debug("starting ticket submitting thread");
+ unifiedTicketingUtil.start();
+
+ if (wait) {
+ LOG.debug("wait for ticket submission completion");
+ unifiedTicketingUtil.join();
+ } else {
+ LOG.debug("tickets will be submitted in background");
+ }
+ }
+
+ public static void reportToTicketsystem(TicketSystem ts, ModocotResultSummary resultSummary) {
+ // tickets existing yet
+ LOG.debug("fetching existing tickets");
+ Set tickets = fetchExistingModocotTickets(ts);
+
+ // for each fail or compile error
+ LOG.debug("start failed tests reporting");
+ resultSummary.failures.forEach(f -> processResult(ts, tickets, f, false));
+ LOG.debug("start compilation errors reporting");
+ resultSummary.compilationErrors.forEach(c -> processResult(ts, tickets, c, true));
+
+ LOG.debug("closing all remaining tickets, no longer appeared");
+ tickets.forEach(ticket -> ticket.close().save());
+ }
+
+ public static Ticket updateTicketFromResult(TicketSystem ts, Ticket ticket, ModocotResult result, boolean compilationError) {
+ ticket
+ .open()
+ .setDescription(createTicketDescriptionFromResult(ts, result, compilationError));
+ if (ts.hasLabelSupport()) ticket.addLabel(MODOCOT_LABEL);
+
+ return ticket.save();
+ }
+}