Java tutorial
/** * Marmoset: a student project snapshot, submission, testing and code review * system developed by the Univ. of Maryland, College Park * * Developed as part of Jaime Spacco's Ph.D. thesis work, continuing effort led * by William Pugh. See http://marmoset.cs.umd.edu/ * * Copyright 2005 - 2011, Univ. of Maryland * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package edu.umd.cs.marmoset.modelClasses; import static edu.umd.cs.marmoset.modelClasses.TestOutcome.TestType.FINDBUGS; import static edu.umd.cs.marmoset.modelClasses.TestOutcome.TestType.PMD; import static edu.umd.cs.marmoset.modelClasses.TestOutcome.TestType.PUBLIC; import static edu.umd.cs.marmoset.modelClasses.TestOutcome.TestType.RELEASE; import static edu.umd.cs.marmoset.modelClasses.TestOutcome.TestType.SECRET; import static edu.umd.cs.marmoset.modelClasses.TestOutcome.TestType.STUDENT; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.URLEncoder; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import javax.annotation.CheckForNull; import javax.annotation.CheckReturnValue; import javax.annotation.meta.TypeQualifier; import org.apache.commons.io.IOUtils; import org.dom4j.DocumentException; import com.google.common.base.Strings; import edu.umd.cs.marmoset.codeCoverage.CodeCoverageResults; import edu.umd.cs.marmoset.codeCoverage.CoverageLevel; import edu.umd.cs.marmoset.codeCoverage.FileWithCoverage; import edu.umd.cs.marmoset.utilities.MarmosetUtilities; import edu.umd.cs.marmoset.utilities.SqlUtilities; import edu.umd.cs.marmoset.utilities.TextFileReader; import edu.umd.cs.marmoset.utilities.XSSScrubber; /** * Object to represent a single test outcome. * * This class is horribly over-loaded in the database in that we * store different things depending on how the class is being used. * For example, we store code coverage results, pass/fail test outcomes, * FindBugs results, and PMD results in the same database table. Thus, * this class contains many getter methods that access the same fields * in order to make programming to this class a little easier. * * TODO Document the various ways in which this class is used. * * @author David Hovemeyer * @author Jaime Spacco */ public class TestOutcome implements Serializable { /** * Comment for <code>serialVersionUID</code> */ private static final long serialVersionUID = 2L; private static final int serialMinorVersion = 1; public static final int FIRST_TEST_NUMBER = id(1); private static int id(int i) { return i; } // Test types @Documented @TypeQualifier(applicableTo = String.class) @Retention(RetentionPolicy.RUNTIME) public @interface OutcomeType { } public static @OutcomeType String asOutcomeType(String s) { return s; } // Outcome types public static final @OutcomeType String FAILED = asOutcomeType("failed"); public static final @OutcomeType String PASSED = asOutcomeType("passed"); public static final @OutcomeType String COULD_NOT_RUN = asOutcomeType("could_not_run"); public static final @OutcomeType String STATIC_ANALYSIS = asOutcomeType("warning"); public static final @OutcomeType String NOT_IMPLEMENTED = asOutcomeType("not_implemented"); public static final @OutcomeType String ERROR = asOutcomeType("error"); public static final @OutcomeType String HUH = asOutcomeType("huh"); public static final @OutcomeType String MISSING_COMPONENT = asOutcomeType("missing_component"); public static final @OutcomeType String TIMEOUT = asOutcomeType("timeout"); public static final @OutcomeType String FORK_BOMB = asOutcomeType("fork_bomb"); public static final @OutcomeType String UNCOVERED_METHOD = asOutcomeType("uncovered_method"); public static enum TestType { BUILD, PUBLIC, RELEASE, SECRET, STUDENT, FINDBUGS, PMD, UNCOVERED_METHOD; public static TestType valueOfAnyCase(String name) { return valueOf(name.toUpperCase()); } public static @CheckForNull TestType valueOfAnyCaseOrNull(String name) { if (Strings.isNullOrEmpty(name)) return null; try { return valueOfAnyCase(name); } catch (Exception e) { return null; } } /** Does this test have a standard score defined by the current test setup */ public boolean isScored() { switch (this) { case PUBLIC: case RELEASE: case SECRET: return true; default: return false; } } public static TestOutcome.TestType[] DYNAMIC_TEST_TYPES = { PUBLIC, RELEASE, SECRET, STUDENT }; @Override public String toString() { return name().toLowerCase(); } public boolean isDynamic() { switch (this) { case PUBLIC: case RELEASE: case SECRET: case STUDENT: return true; default: return false; } } } // Granularity of coverage public static final String METHOD = "METHOD"; public static final String STATEMENT = "STATEMENT"; public static final String BRANCH = "BRANCH"; public static final String NONE = "NONE"; private int testRunPK = 0; private TestType testType; private String testNumber; private @OutcomeType String outcome; private int pointValue; private int executionTimeMillis; private String testName; private String shortTestResult = ""; private String longTestResult = ""; private /* @DottedClassName */ String exceptionClassName; private Object details; private CoverageLevel coarsestCoverageLevel = CoverageLevel.NONE; private boolean exceptionSourceCoveredElsewhere; /** * Cached copy of codeCoverageResults. Field is initialized once which saves a bunch of * extra unzipping of the zipped XML coverage results. */ private transient CodeCoverageResults codeCoverageResults = null; /** * List of all attributes for test_outcomes <code>TEST_OUTCOMES_ATTRIBUTE_NAME_LIST</code> */ static final String[] ATTRIBUTE_NAME_LIST = { "test_run_pk", "test_type", "test_number", "outcome", "point_value", "test_name", "short_test_result", "long_test_result", "exception_class_name", "coarsest_coverage_level", "exception_source_covered_elsewhere", "details", "execution_time_ms" }; static final String[] ATTRIBUTE_NICE_NAME_LIST = { "test_run_pk", "type", "test #", "outcome", "points", "name", "short result", "long result", "exception class name", "coarsest coverage level", "exception source covered elsewhere", "details", "execution time ms" }; /** Name of this table in the database. */ public static final String TABLE_NAME = "test_outcomes"; /** * Fully-qualified attributes for test_outcomes table. */ public static final String ATTRIBUTES = Queries.getAttributeList(TABLE_NAME, ATTRIBUTE_NAME_LIST); public TestOutcome() { } public TestOutcome(ResultSet rs, int col) throws SQLException { fetchValues(rs, col); } @Override public int hashCode() { return testType.hashCode() + MarmosetUtilities.hashString(testName) + MarmosetUtilities.hashString(outcome) + MarmosetUtilities.hashString(shortTestResult) + MarmosetUtilities.hashString(longTestResult); } @Override public boolean equals(Object o) { if (o == null || this.getClass() != o.getClass()) return false; TestOutcome other = (TestOutcome) o; return testType.equals(other.testType) && MarmosetUtilities.stringEquals(testName, other.testName) && MarmosetUtilities.stringEquals(outcome, other.outcome) && MarmosetUtilities.stringEquals(shortTestResult, other.shortTestResult) && MarmosetUtilities.stringEquals(longTestResult, other.longTestResult); } @Override public String toString() { StringBuffer buf = new StringBuffer(); buf.append(testRunPK); buf.append(','); buf.append(testType); buf.append(','); buf.append(testName); buf.append(','); buf.append(outcome); buf.append(','); buf.append(shortTestResult); buf.append(','); buf.append(longTestResult.substring(0, Math.min(longTestResult.length(), 10000)).replace('\n', '|')); buf.append(','); buf.append(coarsestCoverageLevel); buf.append(','); buf.append(exceptionSourceCoveredElsewhere); return buf.toString(); } public String toConciseString() { StringBuffer buf = new StringBuffer(); buf.append(testRunPK); buf.append(','); buf.append(testType); buf.append(','); buf.append(testName); buf.append(','); buf.append(outcome); buf.append(','); return buf.toString(); } public boolean isExceptionSourceApproximatelyCoveredElsewhere(CodeCoverageResults coverage, int range) throws IOException { if (!isError() || !RELEASE.equals(testType)) return false; StackTraceElement stackTraceElement = getExceptionSourceFromLongTestResult(); if (coversLineOrPreviousLines(stackTraceElement, range, coverage)) return true; return false; } private boolean coversLineOrPreviousLines(StackTraceElement stackTraceElement, int range, CodeCoverageResults coverage) throws IOException { // If we weren't able to extract a stack trace for some reason, then simply return false if (stackTraceElement == null) return false; if (!isCoverageType()) return false; FileWithCoverage fileWithCoverage = coverage.getFileWithCoverage(stackTraceElement.getFileName()); if (fileWithCoverage == null) { // ERROR: Can't find the appropriate file in this set of coverage results System.err.println("No coverage for " + stackTraceElement.getFileName()); return false; } for (int ii = stackTraceElement.getLineNumber(); ii >= stackTraceElement.getLineNumber() - range; ii--) { if (fileWithCoverage.isLineCovered(ii)) return true; } return false; } /** * Java-only. * <p> * Is this outcome a fault? * <p> * A fault is any test case that fails but does not fail with the default * UnsupportedOperationException("You must implement this method") that skeleton or * stub implementation throw when they are not implemented. * <p> * A fault in a test case is any failure due to a normal unit test failure, * an exception being thrown in student code, * a timeout (where the buildserver kills a test case that's taking too long), or * a security manager exception. * @return */ public boolean isFault() { if (outcome.equals(FAILED) || outcome.equals(ERROR) || outcome.equals(HUH) || outcome.equals(MISSING_COMPONENT) || outcome.equals(TIMEOUT) || outcome.equals(FORK_BOMB)) return true; return false; } public boolean isError() { return outcome.equals(ERROR); } public boolean isPublicTest() { return getTestType().equals(PUBLIC); } public boolean isStudentTest() { return getTestType().equals(STUDENT); } public boolean isConfidential() { return isReleaseTest() || isSecretTest(); } public boolean isReleaseTest() { return getTestType().equals(RELEASE); } public boolean isSecretTest() { return getTestType().equals(SECRET); } public boolean isPassed() { return getOutcome().equals(TestOutcome.PASSED); } public boolean isFailed() { if (isFault() || outcome.equals(NOT_IMPLEMENTED)) return true; return false; } /** * @return Returns the longTestResult. */ public @CheckReturnValue String getLongTrimmedTestResult() { String result = getLongTestResult(); int i = result.indexOf("at sun.reflect.NativeMethodAccessorImpl.invoke0"); if (i > 0) result = result.substring(0, i).trim(); if (result.length() > MAX_LONG_TEST_RESULT_CHARS_TO_DISPLAY) result = result.substring(0, MAX_LONG_TEST_RESULT_CHARS_TO_DISPLAY) + String.format( "%n ... %d characters trimmed ...", result.length() - MAX_LONG_TEST_RESULT_CHARS_TO_DISPLAY); return result; } /** * @return Returns the longTestResult. */ public String getLongTestResult() { if (isLongTestResultCompressed()) return decompress((byte[]) details); return longTestResult; } /** * @return */ public boolean isLongTestResultCompressed() { return LONG_TEST_RESULTS_ARE_COMPRESSED.equals(longTestResult) && details instanceof byte[]; } /** * @return Returns the longTestResult. */ public @HTML String getLongTestResultAsHtml() { StringBuffer builder = XSSScrubber.scrubbed(getLongTestResult(), false); return XSSScrubber.asHTML(builder.toString().replaceAll("\n", "<br>")); } public static final int MAX_LONG_TEST_RESULT_CHARS_TO_DISPLAY = 5000; public static final int MAX_LONG_TEST_RESULT_CHARS = 60000; public static final String CARDINAL = "cardinal"; /** * Returns The String representation of the code coverage results in XML format. * * TODO Perhaps this should return the CodeCoverageResults object? * * @return Returns The String representation of the code coverage results in XML format. * @throws IOException If the coverage results (currently zipped into a byte * array and stored in the 'details' blob column) cannot be unzipped. */ public String getCodeCoverageXMLResultsAsString() throws IOException { // In very early versions that supported code coverage, I stuck the entire XML // file into the longTestResult field // if (isCoverageType() && !"".equals(longTestResult)) // return longTestResult; // Unzip the code coverage results from the "details" field. // We assume there will only be one entry in the zip archive. ZipInputStream zip = null; BufferedReader reader = null; try { zip = new ZipInputStream(new ByteArrayInputStream((byte[]) details)); ZipEntry entry = zip.getNextEntry(); if (entry == null) throw new IOException( "This test outcome doesn't contain " + "any zipped entries in the 'details' field."); reader = new BufferedReader(new InputStreamReader(zip)); StringBuffer result = new StringBuffer(); while (true) { String line = reader.readLine(); if (line == null) break; result.append(line + "\n"); } return result.toString(); } finally { try { if (reader != null) reader.close(); } finally { if (zip != null) zip.close(); } } } /** * Getter for the codeCoverageResults. Will unzip and parse the results once and * then cache them in an instance variable. * @return The codeCoverageResults for this testOutcome, if any are available. * @throws IOException If there is an error unzipping or parsing the XML document containing * the codeCoverageResults * @throws IllegalStateException If this test outcome lacks codeCoverageResults. */ public CodeCoverageResults getCodeCoverageResults() { if (codeCoverageResults != null) return codeCoverageResults; if (TIMEOUT.equals(getOutcome())) { // Return empty coverage results for a timeout return new CodeCoverageResults(); } // Can't have coverage if not correct type (must be public/release/secret/student) if (!isCoverageType()) throw new IllegalStateException("Lacking code coverage information for test outcome " + this); // If we don't have any code coverage details available, return an empty set. // A test case may have timed out, which produces no coverage. In general // we don't want to fail and throw an exception if we don't have coverage data for // some reason. if (getDetails() == null) return new CodeCoverageResults(); try { // Haven't unzipped and parsed codeCoverageResults yet, so initialize them from // the details field. String xmlStringResults = getCodeCoverageXMLResultsAsString(); return codeCoverageResults = CodeCoverageResults.parseString(xmlStringResults); } catch (DocumentException e) { //throw new IOException("Unable to parse XML file containing CodeCoverageResults: "+e.getMessage()); // If we can't parse things, just return an empty // TODO replace println with a logger! System.err.println("Unable to parse XML file containing CodeCoverageResults for " + toConciseString() + ": " + e.getMessage()); return new CodeCoverageResults(); } catch (IOException e) { System.err.println("Unable to parse XML file containing CodeCoverageResults for " + toConciseString() + ": " + e.getMessage()); return new CodeCoverageResults(); } } /** * Zips the contents of a given file containing code coverage results in XML format. * @param file The file containing the code coverage XML results. * @throws IOException If there are any error reading the file. */ public void setCodeCoveralXMLResults(File file) throws IOException { if (!file.exists() || !file.canRead()) { return; } ByteArrayOutputStream actualData = new ByteArrayOutputStream(); ZipOutputStream zipOut = new ZipOutputStream(actualData); ZipEntry entry = new ZipEntry("coverage"); zipOut.putNextEntry(entry); BufferedInputStream in = new BufferedInputStream(new FileInputStream(file)); try { int bytesRead = 0; byte[] bytes = new byte[2048]; while (true) { int numBytes = in.read(bytes); if (numBytes == -1) break; zipOut.write(bytes, 0, numBytes); bytesRead += numBytes; } zipOut.closeEntry(); zipOut.close(); byte[] outbytes = actualData.toByteArray(); // System.out.println("outbytes.length: "+outbytes.length+ // " and we read: " +bytesRead+ " total bytes"); setDetails(outbytes); } finally { in.close(); } } public @CheckReturnValue String getCappedLongTestResult() { String result = getLongTestResult(); return result.substring(0, Math.min(result.length(), MAX_LONG_TEST_RESULT_CHARS_TO_DISPLAY)); } private static @CheckReturnValue String limitLength(String txt, int maxLength) { if (txt.length() <= maxLength) return txt; return txt.substring(0, maxLength); } public static byte[] compress(String txt) { byte[] rawBYtes = txt.getBytes(); ByteArrayOutputStream out = new ByteArrayOutputStream(); try { GZIPOutputStream gzout = new GZIPOutputStream(out); gzout.write(rawBYtes); gzout.close(); } catch (IOException e) { e.printStackTrace(); return rawBYtes; } return out.toByteArray(); } public static String decompress(byte[] rawBytes) { ByteArrayInputStream bytes = new ByteArrayInputStream(rawBytes); ByteArrayOutputStream out = new ByteArrayOutputStream(); try { GZIPInputStream gzin = new GZIPInputStream(bytes); IO.copyStream(gzin, out); out.close(); } catch (IOException e) { return " -- unable to decompress: " + e.getMessage() + " -- "; } return new String(out.toByteArray()); } public static final String LONG_TEST_RESULTS_ARE_COMPRESSED = "Long test results are compressed (MVRnYVfnYvDotUpeNOHDaD31DlJYkgs)"; public void setLongTestResultCompressIfNeeded(String longTestResult) { if (longTestResult.length() > MAX_LONG_TEST_RESULT_CHARS && details == null) { byte[] bytes = compress(longTestResult); if (bytes.length <= MAX_LONG_TEST_RESULT_CHARS) setDetails(bytes); this.longTestResult = LONG_TEST_RESULTS_ARE_COMPRESSED; return; } setLongTestResult(longTestResult); } /** * @param longTestResult The longTestResult to set. */ public void setLongTestResult(String longTestResult) { // Truncate long test results that are excessively long. These // could cause an OutOfMemory down the line when we serialize // the test results for submission. // // 1 << 16 is the limit imposed by the MySQL text field anyway. // this.longTestResult = limitLength(longTestResult, MAX_LONG_TEST_RESULT_CHARS); } /** * Truncates the longTestResult string to be at most 64K chars * (which is the limit imposed by the MySQL test field anyway). */ public void truncateLongTestResult() { longTestResult = limitLength(longTestResult, MAX_LONG_TEST_RESULT_CHARS); } /** * @return Returns the outcome. */ public @OutcomeType String getOutcome() { return outcome; } /** * @param outcome The outcome to set. */ public void setOutcome(@OutcomeType String outcome) { this.outcome = outcome; } /** * Returns the outcome suitable for viewing by students; basically, this means * mapping ERROR and NOT_IMPLEMENTED to failed so that students always see * outcomes as either PASSED or FAILED (or COULD_NOT_RUN if things time out). * @return the outcome, with "error" and "not_implemented" mapped to "failed" */ public String getStudentOutcome() { if (outcome.equals(HUH)) return TestOutcome.FAILED; return outcome; } /** * @return Returns the pointValue. */ public int getPointValue() { return pointValue; } /** * @param pointValue The pointValue to set. */ public void setPointValue(int pointValue) { this.pointValue = pointValue; } /** * @return Returns the shortTestResult. */ public String getShortTestResult() { return shortTestResult; } /** * @param shortTestResult The shortTestResult to set. */ public void setShortTestResult(String shortTestResult) { this.shortTestResult = limitLength(shortTestResult, 300); } /** * @return Returns the testRunPK. */ public int getTestRunPK() { return testRunPK; } /** * @param testRunPK The testRunPK to set. */ public void setTestRunPK(int testRunPK) { this.testRunPK = testRunPK; } /** * @return Returns the testName. */ public String getTestName() { return testName; } /** * Converts the < and > to & lt and & gt * @return Test name suitable for display as HTML */ public @HTML String getHtmlTestName() { String htmlTestName = testName.replace("<", "<"); htmlTestName = htmlTestName.replace(">", ">"); return XSSScrubber.asHTML(htmlTestName); } /** * @return Returns the short version of the testName. */ public String getShortTestName() { String s = testName; int i = s.indexOf('('); if (i > 0) s = s.substring(0, i); return s; } public static int numLines(String s) { int count = 0; int i = -1; do { i = s.indexOf('\n', i + 1); count++; } while (i >= 0); return count; } private static String urlEncode(String s) { try { return URLEncoder.encode(s, "UTF-8"); } catch (UnsupportedEncodingException e) { return URLEncoder.encode(s); } } public static @HTML String getHotlink(TestOutcome outcome, String viewLink) { if (outcome.getOutcome().equals(NOT_IMPLEMENTED)) return XSSScrubber.asHTML(""); String viewSourceLink = viewLink + "/sourceCode.jsp"; String fullResult = outcome.getLongTestResult(); int length = fullResult.length(); @HTML String result = outcome.getHotlink(viewSourceLink); if (length > 5000) { result = String.format( "<b><a href=\"%s/PrintTestOutcome?testRunPK=%d&testType=%s&testNumber=%s\">full result is %d characters, %d lines long</a></b><br>%s", viewLink, outcome.getTestRunPK(), urlEncode(outcome.getTestType().name()), urlEncode(outcome.getTestNumber()), length, numLines(fullResult), result); } return XSSScrubber.asHTML(result); } private @HTML String getHotlink(String viewSourceLink) { if (testType.equals(FINDBUGS)) return getFindbugsHotlink(viewSourceLink); else if (testType.equals(PMD)) return getPmdLocation(viewSourceLink); else if (testType.isDynamic() || testType.equals(TestType.UNCOVERED_METHOD)) return getStackTraceHotlinks(viewSourceLink); return getLongTestResultAsHtml(); } private static List<String> ignoreSet = new LinkedList<String>(); static { ignoreSet.add("java."); ignoreSet.add("junit."); ignoreSet.add("\\s+sun\\.reflect"); ignoreSet.add("edu.umd.cs.buildServer"); ignoreSet.add("ReleaseTest"); ignoreSet.add("PublicTest"); ignoreSet.add("SecretTest"); ignoreSet.add("SimpleTest"); ignoreSet.add("TestAgainstFile"); ignoreSet.add("SpiderTest"); } /** * Checks if a string matches any regexp that is in the "ignoreSet". The "ignoreSet" is * a hard-coded list of regexps that represent frames of a stack-trace that it doesn't * make sense to try to hotlink to because the source is not available; i.e. anything in * the java.* or junit.* packages, in any of the buildserver classes, etc. * * TODO the ignoreSet should dynamically incorporate the classfiles from the * test.properties file since that makes more sense. * * @param line The line of text to be searched for any of the regexps in the ignoreSet. * @return True if this line should be ignored; false otherwise. */ private static boolean matchesIgnoreSet(String line) { for (String s : ignoreSet) { if (line.contains(s)) return true; } return false; } public static String getExceptionLocation(TestOutcome outcome, String viewSourceLink) { return outcome.getExceptionLocation(viewSourceLink); } public static final Pattern FINDBUGS_LOCATION_REGEX = Pattern.compile("At (\\w+\\.java):\\[line (\\d+)\\]"); public @HTML String getExceptionLocation(String viewSourceLink) { StringBuffer buf = new StringBuffer(); if (isError()) { String testResult = getLongTestResult(); if (Strings.isNullOrEmpty(testResult)) return ""; BufferedReader reader = null; Pattern pattern = FINDBUGS_LOCATION_REGEX; try { reader = new BufferedReader(new StringReader(getLongTrimmedTestResult())); while (true) { String line = reader.readLine(); if (line == null) break; // System.out.println("line: " +line); Matcher matcher = pattern.matcher(line); if (!matchesIgnoreSet(line) && matcher.find() && matcher.groupCount() > 1) { String sourceFileName = matcher.group(1); String startHighlight = matcher.group(2); int numToHighlight = 1; int numContext = 0; buf.append(createSourceCodeLink(viewSourceLink, line, sourceFileName, startHighlight, numToHighlight, numContext)); buf.append("<br>"); // Return after we find the first hot-link-able stack frame return XSSScrubber.asHTML(buf.toString()); } else { buf.append(line + "<br>\n"); } } } catch (IOException ignore) { throw new RuntimeException("DAMMIT JIM!", ignore); } finally { IOUtils.closeQuietly(reader); } } return XSSScrubber.asHTML(buf.toString()); } private @HTML String getFindbugsHotlink(String viewSourceLink) { if (!testType.equals(FINDBUGS)) throw new IllegalArgumentException("is type " + testType + ", not findbugs"); if (shortTestResult == null || shortTestResult.equals("")) return ""; // At ExtractHRefs.java:[line 64] // TODO handle multi-line FB warnings String p = "At (\\w+\\.java):\\[line (\\d+)\\]"; Pattern pattern = Pattern.compile(p); Matcher matcher = pattern.matcher(shortTestResult); if (matcher.matches() && matcher.groupCount() > 1) { String sourceFileName = matcher.group(1); String startHighlight = matcher.group(2); int numToHighlight = 1; int numContext = 0; return createSourceCodeLink(viewSourceLink, shortTestResult, sourceFileName, startHighlight, numToHighlight, numContext); } return XSSScrubber.scrubbedStr(shortTestResult); } public StackTraceElement getExceptionSourceFromLongTestResult() { if (!isError()) throw new IllegalStateException( "Can ONLY get exception source from test outcome of type " + TestOutcome.ERROR); String testResult = getLongTestResult(); if (Strings.isNullOrEmpty(testResult)) throw new IllegalStateException("No stack trace available in this outcome: " + outcome); BufferedReader reader = null; try { reader = new BufferedReader(new StringReader(getLongTrimmedTestResult())); Pattern pattern = Pattern.compile("\\((\\w+\\.java):(\\d+)\\)"); while (true) { String line = reader.readLine(); if (line == null) break; Matcher matcher = pattern.matcher(line); if (!matchesIgnoreSet(line) && matcher.find() && matcher.groupCount() > 1) { return MarmosetUtilities.parseStackTrace(line); } } } catch (IOException ignore) { // Cannot really happen with a stringreader } return null; } private @HTML String getStackTraceHotlinks(String viewSourceLink) { // System.out.println("Calling getStackTraceHotlinks!"); String testResult = getLongTestResult(); if (Strings.isNullOrEmpty(testResult) || getOutcome().equals(NOT_IMPLEMENTED)) return ""; StringBuffer buf = new StringBuffer(); String txt = getLongTrimmedTestResult(); BufferedReader reader = null; Pattern pattern = Pattern.compile("\\((\\w+\\.java):(\\d+)\\)"); try { reader = new BufferedReader(new StringReader(txt)); while (true) { String line = reader.readLine(); if (line == null) break; /** skip stacktracelements resulting from Clover */ String trimmed = line.trim(); if (trimmed.startsWith("at ")) { if (line.contains(".__CLR3_0_") || trimmed.startsWith("at junit.framework.Assert") || trimmed.startsWith("at java.io") || trimmed.startsWith("at sun.") || trimmed.startsWith("at edu.umd.cs.diffText")) continue; } line = line.replaceAll("&", "&"); line = line.replaceAll("<", "<"); line = line.replaceAll(">", ">"); Matcher matcher = pattern.matcher(line); if (!line.contains("java.") && !line.contains("junit.") && !line.contains("\\s+sun\\.reflect") && !line.contains("edu.umd.cs.buildServer") && !line.contains("ReleaseTest") && !line.contains("PublicTest") && !line.contains("SecretTest") && !line.contains("SimpleTest") && !line.contains("TestAgainstFile") && !line.contains("SpiderTest") && matcher.find() && matcher.groupCount() > 1) { String sourceFileName = matcher.group(1); String startHighlight = matcher.group(2); int numToHighlight = 1; int numContext = 0; buf.append(createSourceCodeLink(viewSourceLink, line, sourceFileName, startHighlight, numToHighlight, numContext)); buf.append("<br>"); } else { buf.append(line + "<br>\n"); } } } catch (IOException ignore) { throw new RuntimeException("DAMMIT JIM!", ignore); } finally { IOUtils.closeQuietly(reader); } return XSSScrubber.asHTML(buf.toString()); } /** * @param viewSourceLink TODO * @param line * @param sourceFileName * @param startHighlight * @param numToHighlight * @param numContext * @return */ private @HTML String createSourceCodeLink(String viewSourceLink, String line, String sourceFileName, String startHighlight, int numToHighlight, int numContext) { if (line.startsWith("\tat ")) line = " at " + line.substring(4); int leadingSpaces = 0; while (leadingSpaces < line.length() && (line.charAt(leadingSpaces) == ' ' || line.charAt(leadingSpaces) == '\t')) leadingSpaces++; return XSSScrubber.asHTML(line.substring(0, leadingSpaces) + "<a href=\"" + viewSourceLink + "?testRunPK=" + testRunPK + "&sourceFileName=" + sourceFileName + "&testType=" + (testType.equals(TestOutcome.TestType.UNCOVERED_METHOD) ? "public-student" : testType) + "&testNumber=" + (testType.equals(TestOutcome.TestType.UNCOVERED_METHOD) ? "all" : testNumber) + "&testName=" + testName + "&startHighlight=" + startHighlight + "&numToHighlight=" + numToHighlight + "&numContext=" + numContext + "#codehighlight0\">" + line.substring(leadingSpaces) + "</a>\n"); } /** * @param viewSourceLink TODO * @return Returns the location in the file where the PMD warning occurs. */ private @HTML String getPmdLocation(String viewSourceLink) { String pmdLocation = shortTestResult; int i = pmdLocation.indexOf(':'); if (i > 0) pmdLocation = pmdLocation.substring(i + 1); if (pmdLocation == null || pmdLocation.equals("")) return ""; // src/oop2/searchTree/EmptyTree.java:19 Pattern pattern = Pattern.compile("([\\w/]+\\.java):(\\d+)"); Matcher matcher = pattern.matcher(pmdLocation); if (matcher.matches() && matcher.groupCount() > 1) { String sourceFileName = matcher.group(1); String startHighlight = matcher.group(2); int numToHighlight = 1; int numContext = 0; return createSourceCodeLink(viewSourceLink, pmdLocation, sourceFileName, startHighlight, numToHighlight, numContext); } return XSSScrubber.scrubbedStr(pmdLocation); } /** * @param testName The testName to set. */ public void setTestName(String testName) { this.testName = testName; } /** * @return Returns the testType. */ public TestType getTestType() { return testType; } /** * @param testType The testType to set. */ public void setTestType(TestType testType) { this.testType = testType; } /** * @return Returns the testNumber. */ public String getTestNumber() { return testNumber; } /** * @param testNumber The testNumber to set. */ public void setTestNumber(String testNumber) { this.testNumber = testNumber; } /** * Returns a key suitable for inserting this TestOutcome into a map. * Yes, I could just write a hashCode() method and put objects into a set, * but I don't think sets work with JSPs yet. * @return A key suitable for inserting this testOutcome into a map. */ public String getKey() { return testType + "-" + testNumber; } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { int thisMinorVersion = stream.readInt(); if (thisMinorVersion != serialMinorVersion) throw new IOException("Illegal minor version " + thisMinorVersion + ", expecting minor version " + serialMinorVersion); stream.defaultReadObject(); truncateLongTestResult(); } private void writeObject(ObjectOutputStream stream) throws IOException { truncateLongTestResult(); stream.writeInt(serialMinorVersion); stream.defaultWriteObject(); } public String getExceptionClassName() { return exceptionClassName; } public void setExceptionClassName(String exceptionClassName) { this.exceptionClassName = exceptionClassName; } public int getFindBugsRank() { if (!getTestType().equals(TestType.FINDBUGS)) throw new IllegalStateException("Not a findbugs outcome"); return Integer.parseInt(exceptionClassName); } public String getFindBugsRankDescription() { if (!getTestType().equals(FINDBUGS)) throw new IllegalStateException("Not a findbugs outcome"); int rank = Integer.parseInt(exceptionClassName); if (rank <= 4) return String.format("Scariest (%d)", rank); else if (rank <= 9) return String.format("Scary (%d)", rank); else if (rank <= 14) return String.format("Troubling (%d)", rank); return String.format("Of concern (%d)", rank); } /** * @return Returns the details. */ public Object getDetails() { return details; } /** * @param details The details to set. */ public void setDetails(Object details) { this.details = details; } public int getExecutionTimeMillis() { return executionTimeMillis; } public void setExecutionTimeMillis(long executionTimeMillis) { if (executionTimeMillis > Integer.MAX_VALUE) throw new IllegalArgumentException("Execution time too long: " + executionTimeMillis); this.executionTimeMillis = (int) executionTimeMillis; } /** * @return Returns the coarsestPublicStudentCoverage. */ public CoverageLevel getCoarsestCoverageLevel() { return coarsestCoverageLevel; } /** * @param coarsestPublicStudentCoverage The coarsestPublicStudentCoverage to set. */ public void setCoarsestCoverageLevel(CoverageLevel coarsestPublicStudentCoverage) { this.coarsestCoverageLevel = coarsestPublicStudentCoverage; } /** * @return Returns the exceptionPointCoveredElsewhere. */ public boolean getExceptionSourceCoveredElsewhere() { return exceptionSourceCoveredElsewhere; } /** * @param exceptionPointCoveredElsewhere The exceptionPointCoveredElsewhere to set. */ public void setExceptionSourceCoveredElsewhere(boolean exceptionPointCoveredElsewhere) { this.exceptionSourceCoveredElsewhere = exceptionPointCoveredElsewhere; } private void limitSizes() { truncateLongTestResult(); } /** * Populated a prepared statement starting at a given index with all of the fields * of this model class. * @param stmt the PreparedStatement * @param index the starting index * @return the index of the next open slot in the prepared statement * @throws SQLException */ int putValues(PreparedStatement stmt, int index) throws SQLException { limitSizes(); stmt.setInt(index++, getTestRunPK()); stmt.setString(index++, getTestType().name()); stmt.setString(index++, getTestNumber()); stmt.setString(index++, getOutcome()); stmt.setInt(index++, getPointValue()); stmt.setString(index++, getTestName()); stmt.setString(index++, getShortTestResult()); stmt.setString(index++, longTestResult); stmt.setString(index++, getExceptionClassName()); stmt.setString(index++, (getCoarsestCoverageLevel() != null) ? getCoarsestCoverageLevel().toString() : CoverageLevel.NONE.toString()); stmt.setBoolean(index++, getExceptionSourceCoveredElsewhere()); stmt.setObject(index++, getDetails()); stmt.setInt(index++, getExecutionTimeMillis()); return index; } /** * Updates a row of the database based on its compound primary key (test_run_pk, * test_type, test_number) and updates it. * @param conn the connection to the database * @throws SQLException */ public void update(Connection conn) throws SQLException { String update = " UPDATE " + TABLE_NAME + " SET " + " test_run_pk = ?, " + " test_type = ?," + " test_number = ?, " + " outcome = ?, " + " point_value = ?, " + " test_name = ?, " + " short_test_result = ?, " + " long_test_result = ?," + " exception_class_name = ?, " + " coarsest_coverage_level = ?," + " exception_source_covered_elsewhere = ?, " + " details = ? " + " execution_time_ms = ? " + " WHERE test_run_pk = ? " + " AND test_type = ? " + " AND test_number = ? "; limitSizes(); PreparedStatement stmt = null; try { stmt = conn.prepareStatement(update); int index = putValues(stmt, 1); stmt.setInt(index++, getTestRunPK()); stmt.setString(index++, getTestType().name()); stmt.setString(index++, getTestNumber()); //System.out.println(stmt); stmt.executeUpdate(); } finally { Queries.closeStatement(stmt); } } /** * Populate a TestOutcome from a ResultSet that is positioned * at a row of the test_outcomes table. * * @param rs the ResultSet returned by the database. * @param startingFrom index specifying where to start fetching attributes from; * useful if the row contains attributes from multiple tables * @throws SQLException */ public int fetchValues(ResultSet rs, int startingFrom) throws SQLException { setTestRunPK(rs.getInt(startingFrom++)); setTestType(TestType.valueOfAnyCase(rs.getString(startingFrom++))); setTestNumber(rs.getString(startingFrom++)); setOutcome(asOutcomeType(rs.getString(startingFrom++))); setPointValue(rs.getInt(startingFrom++)); setTestName(rs.getString(startingFrom++)); setShortTestResult(rs.getString(startingFrom++)); setLongTestResult(rs.getString(startingFrom++)); setExceptionClassName(rs.getString(startingFrom++)); setCoarsestCoverageLevel(CoverageLevel.fromString(rs.getString(startingFrom++))); setExceptionSourceCoveredElsewhere(rs.getBoolean(startingFrom++)); setDetails(rs.getObject(startingFrom++)); setExecutionTimeMillis(rs.getInt(startingFrom++)); limitSizes(); return startingFrom++; } /** * Delete all test_outcomes that have the given test_run_pk. * @return the number of rows affected */ public static int deleteByTestRunPK(Integer testRunPK, Connection conn) throws SQLException { String query = " DELETE FROM test_outcomes " + " WHERE test_run_pk = ? "; PreparedStatement stmt = null; try { stmt = conn.prepareStatement(query); SqlUtilities.setInteger(stmt, 1, testRunPK); return stmt.executeUpdate(); } finally { Queries.closeStatement(stmt); } } /** * Is the source of a failed test in the implementation or in the driver code? * @return true if the source of the failure is in the implementation; false if it's in the driver * or cannot be determined */ public boolean isExceptionSourceInTestDriver() { if (!(testType.equals(PUBLIC) || testType.equals(RELEASE) || testType.equals(SECRET))) throw new IllegalStateException("Cannot query source of failure for a testOutcome of type " + testType + " because the source of a failure only makes sense for a public, release or secret test"); // doesn't really make sense to query this information for a passed test // but I'm not sure I should throw an exception here if (outcome.equals(PASSED)) return false; // timeouts, security manager exceptions and normal failure due to assertion failed // exceptions are definitely not interesting if (outcome.equals(HUH) || outcome.equals(FAILED) || outcome.equals(MISSING_COMPONENT) || outcome.equals(TIMEOUT) || outcome.equals(NOT_IMPLEMENTED)) return false; // now parse through the stack trace to see if this is exception originates in // the student implementation code or in BufferedReader reader = null; try { reader = new BufferedReader(new StringReader(getLongTrimmedTestResult())); // throw out the first line of the stacktrace (this is just the exception name) String line = reader.readLine(); if (line == null) { System.err.println("Missing first line of the stack trace for " + this.toString()); return false; } // read the second line of the stack trace-- this is the true source of the exception line = reader.readLine(); if (line == null) { System.err.println("Missing second line of the stack trace for " + this.toString()); return false; } // if the source of the stack trace exception contains a class that looks like // a junit test driver classname, then the source of the exception is the driver // TODO make sure instructors standardize on the names of the junit test suites // i.e. always PublicTests, ReleaseTests, SecretTests if (line.contains("ReleaseTest") || line.contains("PublicTest") || line.contains("SecretTest") || line.contains("SimpleTest") || line.contains("TestAgainstFile") || line.contains("SpiderTest")) { //System.err.println("matching line: " +line); return true; } return false; } catch (IOException e) { throw new RuntimeException("A readLine() to a StringReader failed! ", e); } finally { IOUtils.closeQuietly(reader); } } /** * Does this testOutcome have coverage information avaiable? * <p> * Must be a test type with a non-null details field containing zipped XML * of the coverage results. * @return True if this testOutcome contains coverage information; false otherwise. */ public boolean isCoverageType() { return isCardinalTestType() || isStudentTestType(); } /** * Is this testOutcome one of the cardinal test types (public, release, secret)? * @return True if this testOutcome is a test type (i.e. a public, release or secret * outcome); false if it it some other type of testOutcome (i.e. a FindBugs warning * or a student-written test). */ public boolean isCardinalTestType() { return testType.isScored(); } /** * Is this testOutcome the result of executing a student-written test case? * @return True if this testOutcome is a student-written test; false otherwise. */ public boolean isStudentTestType() { return testType.equals(STUDENT); } public boolean coversFileAtLineNumber(String fileName, int lineNumber) throws IOException { // Code coverage is stored as the zipped XML of the coverage results in // the details field of the TestOutcome object. Yes, this is specific // to coverage tools that use XML (which seems to be most of them) // and perhaps I could do it a more robust way. But that's how it is for now. // Can't have code coverage if this is not a test outcome with // coverage information available. if (!isCoverageType()) throw new IllegalStateException("Cannot call this method on a testType lacking coverage information! "); CodeCoverageResults codeCoverageResults = getCodeCoverageResults(); return codeCoverageResults.coversFileAtLineNumber(fileName, lineNumber); } /** * @param requestedSourceFileName * @param requestedLineNumber * @return */ public boolean isStackTraceAtLineForFile(String requestedSourceFileName, int requestedLineNumber) { if (!isCardinalTestType()) return false; if (longTestResult == null || longTestResult.equals("")) return false; BufferedReader reader = null; Pattern pattern = Pattern.compile("\\((\\w+\\.java):(\\d+)\\)"); try { reader = new BufferedReader(new StringReader(getLongTrimmedTestResult())); while (true) { String line = reader.readLine(); if (line == null) break; //System.out.println("line: " +line); Matcher matcher = pattern.matcher(line); if (!line.contains("java.") && !line.contains("junit.") && !line.contains("\\s+sun\\.reflect") && !line.contains("edu.umd.cs.buildServer") && !line.contains("ReleaseTest") && !line.contains("PublicTest") && !line.contains("SecretTest") && !line.contains("SimpleTest") && !line.contains("TestAgainstFile") && !line.contains("SpiderTest") && matcher.find() && matcher.groupCount() > 1) { String sourceFileName = matcher.group(1); String lineNumber = matcher.group(2); if (requestedSourceFileName.equals(sourceFileName) && Integer.valueOf(lineNumber).intValue() == requestedLineNumber) return true; } } } catch (IOException ignore) { // cannot happen; we're reading from a String! } finally { IOUtils.closeQuietly(reader); } return false; } public boolean isFindBugsWarning() { return testType.equals(FINDBUGS); } public boolean isFindBugsWarningAtLine(String requestedSourceFileName, int requestedLineNumber) { if (!testType.equals(FINDBUGS)) return false; // TODO handle multi-line FB warnings Pattern pattern = Pattern.compile("At (\\w+\\.java):\\[line (\\d+)\\]"); Matcher matcher = pattern.matcher(shortTestResult); if (matcher.matches() && matcher.groupCount() > 1) { String sourceFileName = matcher.group(1); String lineNumber = matcher.group(2); if (requestedSourceFileName.equals(sourceFileName) && requestedLineNumber == Integer.parseInt(lineNumber)) { return true; } } return false; } public static FileNameLineNumberPair getFileNameLineNumberPair(String shortTestResult) { // TODO handle multi-line FB warnings, and FB warnings w/ no line number Pattern pattern = Pattern.compile("At (\\w+\\.java):\\[line (\\d+)\\]"); Matcher matcher = pattern.matcher(shortTestResult); if (matcher.matches() && matcher.groupCount() > 1) { String sourceFileName = matcher.group(1); String lineNumber = matcher.group(2); return new FileNameLineNumberPair(sourceFileName, lineNumber); } return null; } public StackTraceElement getInnermostStackTraceElement() { if (isError()) { if (longTestResult == null || longTestResult.equals("")) return null; TextFileReader reader = null; try { reader = new TextFileReader(new StringReader(getLongTrimmedTestResult())); for (String line : reader) { // System.out.println("line: " +line); StackTraceElement element = MarmosetUtilities.parseStackTrace(line); if (element != null) return element; } } catch (IOException ignore) { throw new RuntimeException("DAMMIT JIM!", ignore); } finally { try { if (reader != null) reader.close(); } catch (IOException ignore) { // ignore } } } return null; } public FileNameLineNumberPair getFileNameLineNumberPair() { if (getTestType().equals(FINDBUGS)) { return getFileNameLineNumberPair(getShortTestResult()); } else if (getTestType().equals(PUBLIC) || getTestType().equals(RELEASE) || getTestType().equals(SECRET)) { BufferedReader reader = null; Pattern pattern = Pattern.compile("\\((\\w+\\.java):(\\d+)\\)"); try { reader = new BufferedReader(new StringReader(getLongTrimmedTestResult())); while (true) { String line = reader.readLine(); if (line == null) break; // skip over frames that we won't be able to link to Matcher matcher = pattern.matcher(line); if (!line.contains("java.") && !line.contains("junit.") && !line.contains("\\s+sun\\.reflect") && !line.contains("edu.umd.cs.buildServer") && !line.contains("ReleaseTest") && !line.contains("PublicTest") && !line.contains("SecretTest") && !line.contains("SimpleTest") && !line.contains("TestAgainstFile") && !line.contains("SpiderTest") && matcher.find() && matcher.groupCount() > 1) { String sourceFileName = matcher.group(1); String lineNumber = matcher.group(2); return new FileNameLineNumberPair(sourceFileName, lineNumber); } } } catch (IOException ignore) { // cannot happen; reading from a String! } finally { IOUtils.closeQuietly(reader); } // XXX Does it make sense here to return a singleton representing nothing? return null; } else { throw new IllegalStateException( "You cannot get the filename and line number " + " of a test outcome of type " + getTestType()); } } }