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(); + } +}