package fitnesse.responders.testHistory; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.xml.sax.SAXException; import fitnesse.reporting.history.InvalidReportException; import fitnesse.reporting.history.TestExecutionReport; import fitnesse.testsystems.slim.HtmlTableScanner; public class HistoryComparer { // min for match is .8 content score + .2 topology bonus. static final double MIN_MATCH_SCORE = .8; static final double MAX_MATCH_SCORE = 1.2; private static final String blankTable = "<table><tr><td></td></tr></table>"; private HtmlTableScanner firstScanner; private HtmlTableScanner secondScanner; String firstFileContent = ""; String secondFileContent = ""; List<String> firstTableResults = new ArrayList<>(); List<String> secondTableResults = new ArrayList<>(); List<MatchedPair> matchedTables = new ArrayList<>(); List<String> resultContent = new ArrayList<>(); public String getFileContent(String filePath) throws IOException, SAXException, InvalidReportException { return attemptGetFileContent(filePath); } private String attemptGetFileContent(String filePath) throws IOException, SAXException, InvalidReportException { TestExecutionReport report = readTestExecutionReport(filePath); if (!exactlyOneReport(report)) return null; return report.getContentsOfReport(0); } private TestExecutionReport readTestExecutionReport(String filePath) throws IOException, SAXException, InvalidReportException { return new TestExecutionReport(new File(filePath)); } private boolean exactlyOneReport(TestExecutionReport report) { return report.getResults().size() == 1; } public double findScoreByFirstTableIndex(int firstIndex) { for (MatchedPair match : matchedTables) if (match.first == firstIndex) return match.matchScore; return 0.0; } public String findScoreByFirstTableIndexAsStringAsPercent(int firstIndex) { double score = findScoreByFirstTableIndex(firstIndex); return String.format("%10.2f", (score / MAX_MATCH_SCORE) * 100); } public boolean allTablesMatch() { return matchesAreNotNull() && thereAreEnoughMatches() && allMatchScoresAreHigh(); } private boolean matchesAreNotNull() { return matchedTables != null && firstTableResults != null; } private boolean thereAreEnoughMatches() { return !matchedTables.isEmpty() && matchedTables.size() == firstTableResults.size(); } private boolean allMatchScoresAreHigh() { for (MatchedPair match : matchedTables) { if (match.matchScore < (MAX_MATCH_SCORE - .01)) return false; } return true; } public boolean compare(String firstFilePath, String secondFilePath) throws IOException, SAXException, InvalidReportException { if (firstFilePath.equals(secondFilePath)) return false; initializeFileContents(firstFilePath, secondFilePath); return grabAndCompareTablesFromHtml(); } public boolean grabAndCompareTablesFromHtml() { initializeComparerHelpers(); if (firstScanner.getTableCount() == 0 || secondScanner.getTableCount() == 0) return false; TableListComparer comparer = new TableListComparer(firstScanner, secondScanner); comparer.compareAllTables(); matchedTables = comparer.tableMatches; getTableTextFromScanners(); lineUpTheTables(); addBlanksToUnmatchingRows(); makePassFailResultsFromMatches(); return true; } private void initializeComparerHelpers() { matchedTables = new ArrayList<>(); resultContent = new ArrayList<>(); firstScanner = new HtmlTableScanner(firstFileContent); secondScanner = new HtmlTableScanner(secondFileContent); } public void lineUpTheTables() { for (int currentMatch = 0; currentMatch < matchedTables.size(); currentMatch++) lineUpMatch(currentMatch); lineUpLastRow(); } private void lineUpMatch(int currentMatch) { insertBlanksUntilMatchLinesUp(new FirstResultAdjustmentStrategy(), currentMatch); insertBlanksUntilMatchLinesUp(new SecondResultAdjustmentStrategy(), currentMatch); } private void insertBlanksUntilMatchLinesUp(ResultAdjustmentStrategy adjustmentStrategy, int currentMatch) { while (adjustmentStrategy.matchIsNotLinedUp(currentMatch)) { adjustmentStrategy.insertBlankTableBefore(currentMatch); incrementRemaingMatchesToCompensateForInsertion(adjustmentStrategy, currentMatch); } } private void incrementRemaingMatchesToCompensateForInsertion(ResultAdjustmentStrategy adjustmentStrategy, int currentMatch) { for (int matchToAdjust = currentMatch; matchToAdjust < matchedTables.size(); matchToAdjust++) { matchedTables.set(matchToAdjust, adjustmentStrategy.getAdjustedMatch(matchToAdjust)); } } private interface ResultAdjustmentStrategy { boolean matchIsNotLinedUp(int matchIndex); void insertBlankTableBefore(int matchIndex); MatchedPair getAdjustedMatch(int matchIndex); } private class FirstResultAdjustmentStrategy implements ResultAdjustmentStrategy { @Override public boolean matchIsNotLinedUp(int matchIndex) { MatchedPair matchedPair = matchedTables.get(matchIndex); return matchedPair.first < matchedPair.second; } @Override public void insertBlankTableBefore(int matchIndex) { firstTableResults.add(matchedTables.get(matchIndex).first, blankTable); } @Override public MatchedPair getAdjustedMatch(int matchIndex) { MatchedPair matchedPair = matchedTables.get(matchIndex); return new MatchedPair(matchedPair.first + 1, matchedPair.second, matchedPair.matchScore); } } private class SecondResultAdjustmentStrategy implements ResultAdjustmentStrategy { @Override public boolean matchIsNotLinedUp(int matchIndex) { MatchedPair matchedPair = matchedTables.get(matchIndex); return matchedPair.first > matchedPair.second; } @Override public void insertBlankTableBefore(int matchIndex) { secondTableResults.add(matchedTables.get(matchIndex).second, blankTable); } @Override public MatchedPair getAdjustedMatch(int matchIndex) { MatchedPair matchedPair = matchedTables.get(matchIndex); return new MatchedPair(matchedPair.first, matchedPair.second + 1, matchedPair.matchScore); } } private void lineUpLastRow() { while (firstTableResults.size() > secondTableResults.size()) secondTableResults.add(blankTable); while (secondTableResults.size() > firstTableResults.size()) firstTableResults.add(blankTable); } public void addBlanksToUnmatchingRows() { for (int tableIndex = 0; tableIndex < firstTableResults.size(); tableIndex++) { if (tablesDontMatchAndArentBlank(tableIndex)) { insetBlanksToSplitTheRow(tableIndex); incrementMatchedPairsIfBelowTheInsertedBlank(tableIndex); } } } private boolean tablesDontMatchAndArentBlank(int tableIndex) { return !thereIsAMatchForTableWithIndex(tableIndex) && firstAndSecondTableAreNotBlank(tableIndex); } private boolean thereIsAMatchForTableWithIndex(int tableIndex) { return findScoreByFirstTableIndex(tableIndex) > 0.1; } private boolean firstAndSecondTableAreNotBlank(int tableIndex) { return !(firstTableResults.get(tableIndex).equals(blankTable) || secondTableResults.get(tableIndex).equals(blankTable)); } private void incrementMatchedPairsIfBelowTheInsertedBlank(int tableIndex) { for (int j = 0; j < matchedTables.size(); j++) { MatchedPair match = matchedTables.get(j); if (match.first > tableIndex) matchedTables.set(j, new MatchedPair(match.first + 1, match.second + 1, match.matchScore)); } } private void insetBlanksToSplitTheRow(int tableIndex) { secondTableResults.add(tableIndex, blankTable); firstTableResults.add(tableIndex + 1, blankTable); } private void getTableTextFromScanners() { firstTableResults = new ArrayList<>(); secondTableResults = new ArrayList<>(); for (int i = 0; i < firstScanner.getTableCount(); i++) firstTableResults.add(firstScanner.getTable(i).toHtml()); for (int i = 0; i < secondScanner.getTableCount(); i++) secondTableResults.add(secondScanner.getTable(i).toHtml()); } public void makePassFailResultsFromMatches() { for (int i = 0; i < firstTableResults.size(); i++) { String result = "fail"; for (MatchedPair match : matchedTables) if (match.first == i && match.matchScore >= 1.19) result = "pass"; resultContent.add(result); } } private void initializeFileContents(String firstFilePath, String secondFilePath) throws IOException, SAXException, InvalidReportException { String content = getFileContent(firstFilePath); firstFileContent = content == null ? "" : content; content = getFileContent(secondFilePath); secondFileContent = content == null ? "" : content; } public List<String> getResultContent() { return resultContent; } static class MatchedPair { int first; int second; double matchScore; public MatchedPair(Integer first, Integer second, double matchScore) { this.first = first; this.second = second; this.matchScore = matchScore; } @Override public String toString() { return "[first: " + first + ", second: " + second + ", matchScore: " + matchScore + "]"; } @Override public int hashCode() { return this.first + this.second; } @Override public boolean equals(Object obj) { if (obj == null) return false; if (getClass() != obj.getClass()) return false; MatchedPair match = (MatchedPair) obj; return (this.first == match.first && this.second == match.second); } } }