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 (f.getLastReceivedItemCount() >= 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(result.name.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.results.stream() .filter(r -> r.state == ModocotResult.State.FAILURE.ordinal()) .forEach(f -> processResult(ts, tickets, f, false)); LOG.debug("start compilation errors reporting"); resultSummary.results.stream() .filter(r -> r.state == ModocotResult.State.COMPILATIONERROR.ordinal()) .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() .setTitle(createTicketTitleFromResult(ts, result, compilationError)) .setDescription(createTicketDescriptionFromResult(ts, result, compilationError)); if (ts.hasLabelSupport()) ticket.addLabel(MODOCOT_LABEL); return ticket.save(); } }