diff --git a/change-proneness-ranker/src/main/java/org/hjug/git/GitLogReader.java b/change-proneness-ranker/src/main/java/org/hjug/git/GitLogReader.java
index ef197ab3..5b1c839d 100644
--- a/change-proneness-ranker/src/main/java/org/hjug/git/GitLogReader.java
+++ b/change-proneness-ranker/src/main/java/org/hjug/git/GitLogReader.java
@@ -56,6 +56,29 @@ public File getGitDir(File basedir) {
return repositoryBuilder.getGitDir();
}
+ /**
+ * Returns the current commit hash at HEAD
+ *
+ * @return the commit hash as a String, or null if HEAD cannot be resolved
+ * @throws IOException if an I/O error occurs
+ */
+ public String getCurrentCommitHash() throws IOException {
+ ObjectId headCommit = gitRepository.resolve("HEAD");
+ if (headCommit == null) {
+ return null;
+ }
+ return headCommit.getName();
+ }
+
+ /**
+ * Returns the repository's origin URL
+ *
+ * @return the origin URL as a String, or null if not configured
+ */
+ public String getOriginUrl() {
+ return gitRepository.getConfig().getString("remote", "origin", "url");
+ }
+
// log --follow implementation may be worth adopting in the future
// https://github.com/spearce/jgit/blob/master/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevWalkTextBuiltin.java
diff --git a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java
index aaf3983a..8fb8dff5 100644
--- a/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java
+++ b/refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java
@@ -1,6 +1,8 @@
package org.hjug.mavenreport;
import java.util.*;
+
+import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.maven.doxia.markup.HtmlMarkup;
import org.apache.maven.doxia.sink.Sink;
@@ -63,6 +65,7 @@ public String getDescription(Locale locale) {
+ " have the highest priority values.";
}
+ @SneakyThrows
@Override
public void executeReport(Locale locale) {
HtmlReport htmlReport = new HtmlReport();
diff --git a/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java b/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java
index bd46d220..c21c1b4f 100644
--- a/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java
+++ b/report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java
@@ -1,13 +1,11 @@
package org.hjug.refactorfirst.report;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
+import java.util.*;
import lombok.extern.slf4j.Slf4j;
import org.hjug.cbc.RankedCycle;
import org.hjug.cbc.RankedDisharmony;
import org.hjug.gdg.GraphDataGenerator;
+import org.hjug.graphbuilder.CodebaseGraphDTO;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultWeightedEdge;
@@ -417,6 +415,17 @@ public String printTitle(String projectName, String projectVersion) {
return "
Refactor First Report for " + projectName + " " + projectVersion + " \n";
}
+ @Override
+ StringBuilder createMenu(
+ List disharmonySpecs,
+ Map> rankedDisharmoniesByAnchor,
+ List rankedCycles) {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("Class Map \n");
+ stringBuilder.append(super.createMenu(disharmonySpecs, rankedDisharmoniesByAnchor, rankedCycles));
+ return stringBuilder;
+ }
+
@Override
String renderGithubButtons() {
return "\n" + "Show RefactorFirst some ❤️\n"
@@ -465,8 +474,8 @@ String renderDisharmonyChart(String anchorId, String title, List
Class Map");
+ stringBuilder.append("");
stringBuilder.append("\n");
@@ -501,7 +510,9 @@ private StringBuilder generateGraphButtons(String graphName, String dot) {
stringBuilder.append("\nRed lines represent relationships to remove. \n");
stringBuilder.append("Red nodes represent classes to remove. \n");
- stringBuilder.append("Zoom in / out with your mouse wheel and click/move to drag the image.\n");
+ stringBuilder.append("Zoom in / out with your mouse wheel and click/move to drag the image. \n");
+ stringBuilder.append(
+ "Clicking on a node in the DOT graph (if present below) will open its source file in the repo. It will not open a new browser window.\n");
stringBuilder.append("
\n");
return stringBuilder;
}
@@ -511,7 +522,7 @@ private static String generateDotImage(String graphName) {
return "
\n"
+ "\n";
}
- String buildClassGraphDot(Graph classGraph) {
+ String buildClassGraphDot(
+ Graph classGraph, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) {
StringBuilder dot = new StringBuilder();
dot.append("`strict digraph G {\n");
@@ -563,17 +575,26 @@ String buildClassGraphDot(Graph classGraph) {
dot.append(className.replace("$", "_"));
+ dot.append(" [");
+ dot.append(hyperlinkClassForDot(vertex, repoUrl, codebaseGraphDTO));
+
if (vertexesToRemove.contains(vertex)) {
- dot.append(" [color=red style=filled]\n");
+ dot.append(" color=red style=filled");
}
- dot.append(";\n");
+ dot.append("];\n");
}
dot.append("}`;");
return dot.toString();
}
+ String hyperlinkClassForDot(String fqClassName, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) {
+ StringBuilder sb = new StringBuilder();
+ String path = codebaseGraphDTO.getClassToSourceFilePathMapping().get(fqClassName);
+ return sb.append("URL=\"" + repoUrl + path + "\" target=\"_blank\"").toString();
+ }
+
private void renderEdge(
Graph classGraph, DefaultWeightedEdge edge, StringBuilder dot) {
// render edge
@@ -622,8 +643,8 @@ private void renderEdge(
}
@Override
- public String renderCycleVisuals(RankedCycle cycle) {
- String dot = buildCycleDot(classGraph, cycle);
+ public String renderCycleVisuals(RankedCycle cycle, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) {
+ String dot = buildCycleDot(classGraph, cycle, repoUrl, codebaseGraphDTO);
String cycleName = getClassName(cycle.getCycleName()).replace("$", "_");
@@ -643,7 +664,11 @@ public String renderCycleVisuals(RankedCycle cycle) {
return stringBuilder.toString();
}
- String buildCycleDot(Graph classGraph, RankedCycle cycle) {
+ String buildCycleDot(
+ Graph classGraph,
+ RankedCycle cycle,
+ String repoUrl,
+ CodebaseGraphDTO codebaseGraphDTO) {
StringBuilder dot = new StringBuilder();
dot.append("`strict digraph G {\n");
@@ -653,18 +678,29 @@ String buildCycleDot(Graph classGraph, RankedCycle
// render vertices
for (String vertex : cycle.getVertexSet()) {
- dot.append(getClassName(vertex).replace("$", "_"));
+ String className = getClassName(vertex);
+
+ // if the vertex is a nested class and has no outgoing edges, skip it
+ if (className.contains("$")
+ && className.split("\\$")[className.split("\\$").length - 1].matches("\\d+")
+ && classGraph.outDegreeOf(vertex) == 0) {
+ continue;
+ }
+
+ dot.append(className.replace("$", "_"));
+
+ dot.append(" [");
+ dot.append(hyperlinkClassForDot(vertex, repoUrl, codebaseGraphDTO));
if (vertexesToRemove.contains(vertex)) {
- dot.append(" [color=red style=filled]\n");
+ dot.append(" color=red style=filled");
}
- dot.append(";\n");
+ dot.append("];\n");
}
dot.append("}`;");
-
- return dot.toString().replace("$", "_");
+ return dot.toString();
}
String generate2DPopup(String cycleName) {
diff --git a/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java b/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java
index f8422cf1..b17a8739 100644
--- a/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java
+++ b/report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java
@@ -13,6 +13,7 @@
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.hjug.cbc.*;
import org.hjug.dsm.CircularReferenceChecker;
@@ -23,6 +24,7 @@
import org.hjug.feedback.vertex.kernelized.EnhancedParameterComputer;
import org.hjug.git.GitLogReader;
import org.hjug.graphbuilder.CodebaseGraphDTO;
+import org.hjug.graphbuilder.metrics.DisharmonyMetric;
import org.hjug.graphbuilder.metrics.DisharmonyTypes;
import org.hjug.metrics.DisharmonyInstance;
import org.jgrapht.Graph;
@@ -63,6 +65,7 @@ public class SimpleHtmlReport {
.setMinifyCss(true)
.build();
+ @SneakyThrows
public void execute(
int edgeAnalysisCount,
boolean analyzeCycles,
@@ -116,7 +119,8 @@ public StringBuilder generateReport(
String testSourceDirectory,
String projectName,
String projectVersion,
- File baseDir) {
+ File baseDir)
+ throws Exception {
if (testSourceDirectory == null || testSourceDirectory.isEmpty()) {
testSourceDirectory = "src" + File.separator + "test";
@@ -285,6 +289,11 @@ public StringBuilder generateReport(
boolean hasAnyDisharmony =
!edgesToRemove.isEmpty() || !rankedCycles.isEmpty() || !rankedDisharmoniesByAnchor.isEmpty();
+ String repoUrl;
+ try (GitLogReader glr = new GitLogReader(new File(projectBaseDir))) {
+ repoUrl = glr.getOriginUrl().replace(".git", "") + "/blob/" + glr.getCurrentCommitHash() + "/";
+ }
+
if (!hasAnyDisharmony) {
stringBuilder
.append("Congratulations! ")
@@ -292,51 +301,25 @@ public StringBuilder generateReport(
.append(" ")
.append(projectVersion)
.append(" has no Cycles or Disharmonies!
");
- stringBuilder.append(renderClassGraphVisuals());
+ stringBuilder.append(renderClassGraphVisuals(repoUrl, codebaseGraphDTO));
stringBuilder.append(renderGithubButtons());
log.info("Done! No Disharmonies found!");
return stringBuilder;
}
stringBuilder.append("\n" + "\n" + " \n");
-
- if (!edgesToRemove.isEmpty()) {
- stringBuilder.append("Edges To Remove \n");
- }
-
- if (!disharmonySpecs.isEmpty()) {
- stringBuilder.append("Disharmonies \n" + " ");
- }
-
- for (DisharmonySpec spec : disharmonySpecs) {
- if (rankedDisharmoniesByAnchor.containsKey(spec.anchorId())) {
- stringBuilder
- .append("")
- .append(spec.title())
- .append(" \n");
- }
- }
-
- if (!disharmonySpecs.isEmpty()) {
- stringBuilder.append(" \n" + " ");
- }
-
- if (!rankedCycles.isEmpty()) {
- stringBuilder.append("Class Cycles \n");
- }
-
+ stringBuilder.append(createMenu(disharmonySpecs, rankedDisharmoniesByAnchor, rankedCycles));
stringBuilder.append(" \n" + " \n" + " \n");
+
log.info("Generating HTML Report");
- stringBuilder.append(renderClassGraphVisuals());
+ stringBuilder.append(renderClassGraphVisuals(repoUrl, codebaseGraphDTO));
stringBuilder.append(" \n");
stringBuilder.append(renderGithubButtons());
stringBuilder.append(" \n");
if (!edgeDisharmonies.isEmpty()) {
- stringBuilder.append(renderEdgeDisharmonies(edgeDisharmonies));
+ stringBuilder.append(renderEdgeDisharmonies(edgeDisharmonies, repoUrl, codebaseGraphDTO));
stringBuilder.append(" \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n");
}
@@ -344,29 +327,67 @@ public StringBuilder generateReport(
List rankedForType = rankedDisharmoniesByAnchor.get(spec.anchorId());
if (rankedForType != null && !rankedForType.isEmpty()) {
stringBuilder.append(renderDisharmonyInfo(
- spec.anchorId(), spec.title(), spec.methodLevel(), showDetails, rankedForType));
+ repoUrl, spec.anchorId(), spec.title(), spec.methodLevel(), showDetails, rankedForType));
stringBuilder.append(" \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n");
}
}
if (!rankedCycles.isEmpty()) {
- stringBuilder.append(renderCycles(rankedCycles));
+ stringBuilder.append(renderCycles(rankedCycles, repoUrl, codebaseGraphDTO));
}
log.debug(stringBuilder.toString());
return stringBuilder;
}
- private String renderCycles(List rankedCycles) {
+ StringBuilder createMenu(
+ List disharmonySpecs,
+ Map> rankedDisharmoniesByAnchor,
+ List rankedCycles) {
+ StringBuilder menu = new StringBuilder();
+ if (!edgesToRemove.isEmpty()) {
+ menu.append("Edges To Remove \n");
+ }
+
+ if (!disharmonySpecs.isEmpty()) {
+ menu.append("Disharmonies \n" + " ");
+ }
+
+ for (DisharmonySpec spec : disharmonySpecs) {
+ if (rankedDisharmoniesByAnchor.containsKey(spec.anchorId())) {
+ menu.append("")
+ .append(spec.title())
+ .append(" \n");
+ }
+ }
+
+ if (!disharmonySpecs.isEmpty()) {
+ menu.append(" \n" + " ");
+ }
+
+ if (!rankedCycles.isEmpty()) {
+ menu.append("Class Cycles \n");
+ menu.append("Cycle Map \n");
+ }
+ return menu;
+ }
+
+ private String renderCycles(List rankedCycles, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(renderClassCycleSummary(rankedCycles));
- rankedCycles.stream().limit(1).map(this::renderSingleCycle).forEach(stringBuilder::append);
+ rankedCycles.stream()
+ .limit(1)
+ .map((RankedCycle cycle) -> renderSingleCycle(cycle, repoUrl, codebaseGraphDTO))
+ .forEach(stringBuilder::append);
return stringBuilder.toString();
}
- private String renderEdgeDisharmonies(List edgeDisharmonies) {
+ private String renderEdgeDisharmonies(
+ List edgeDisharmonies, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(
@@ -379,7 +400,9 @@ private String renderEdgeDisharmonies(List edgeDisharmonies) {
.append("Number of Relationships to Remove: ")
.append(edgesToRemove.size())
.append(" \n");
- stringBuilder.append("Classes in bold should be broken apart").append(" \n");
+ stringBuilder
+ .append("Classes with * should be broken apart")
+ .append(" \n");
stringBuilder.append(" \n");
// Content
@@ -396,7 +419,7 @@ private String renderEdgeDisharmonies(List edgeDisharmonies) {
for (RankedDisharmony edge : edgeDisharmonies) {
stringBuilder.append("\n");
- for (String rowData : getEdgeDisharmony(edge)) {
+ for (String rowData : getEdgeDisharmony(edge, repoUrl, codebaseGraphDTO)) {
stringBuilder.append(drawTableCell(rowData));
}
@@ -421,9 +444,9 @@ private String[] getEdgeDisharmonyTableHeadings() {
};
}
- private String[] getEdgeDisharmony(RankedDisharmony edgeInfo) {
+ private String[] getEdgeDisharmony(RankedDisharmony edgeInfo, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) {
return new String[] {
- renderEdge(edgeInfo.getEdge()),
+ renderEdge(edgeInfo.getEdge(), repoUrl, codebaseGraphDTO),
String.valueOf(edgeInfo.getPriority()),
String.valueOf(edgeInfo.getCycleCount()),
String.valueOf(edgeInfo.getEffortRank()),
@@ -508,6 +531,39 @@ private String renderEdge(DefaultWeightedEdge edge) {
.toString();
}
+ private String renderEdge(DefaultWeightedEdge edge, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) {
+ StringBuilder edgesToCut = new StringBuilder();
+ String[] vertexes = extractVertexes(edge);
+
+ String startVertex = vertexes[0].trim();
+ String start;
+ if (vertexesToRemove.contains(startVertex)) {
+ start = hyperlinkClass(startVertex, repoUrl, codebaseGraphDTO) + "* ";
+ } else {
+ start = hyperlinkClass(startVertex, repoUrl, codebaseGraphDTO);
+ }
+
+ String endVertex = vertexes[1].trim();
+ String end;
+ if (vertexesToRemove.contains(endVertex)) {
+ end = hyperlinkClass(endVertex, repoUrl, codebaseGraphDTO) + "* ";
+ } else {
+ end = hyperlinkClass(endVertex, repoUrl, codebaseGraphDTO);
+ }
+
+ // → is HTML "Right Arrow" code
+ return edgesToCut
+ .append(start + " → " + end + " : " + (int) classGraph.getEdgeWeight(edge))
+ .toString();
+ }
+
+ String hyperlinkClass(String className, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) {
+ StringBuilder sb = new StringBuilder();
+ String path = codebaseGraphDTO.getClassToSourceFilePathMapping().get(className);
+ return sb.append("" + getClassName(className) + " ")
+ .toString();
+ }
+
private String[] getCycleSummaryTableHeadings() {
return new String[] {"Cycle Name", "Priority", "Class Count", "Relationship Count" /*, "Minimum Cuts"*/};
}
@@ -522,7 +578,7 @@ private String[] getRankedCycleSummaryData(RankedCycle rankedCycle, StringBuilde
};
}
- private String renderSingleCycle(RankedCycle cycle) {
+ private String renderSingleCycle(RankedCycle cycle, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(" \n");
@@ -531,15 +587,16 @@ private String renderSingleCycle(RankedCycle cycle) {
stringBuilder.append(" \n");
stringBuilder.append(" \n");
- stringBuilder.append(
- "Largest Class Cycle : " + getClassName(cycle.getCycleName()) + " \n");
+ stringBuilder.append("Largest Class Cycle : "
+ + getClassName(cycle.getCycleName()) + " \n");
stringBuilder.append(
"Limiting number of cycles displayed to 1 to keep page load time fast \n");
- stringBuilder.append(renderCycleVisuals(cycle));
+ stringBuilder.append(renderCycleVisuals(cycle, repoUrl, codebaseGraphDTO));
stringBuilder.append("");
stringBuilder.append("
");
- stringBuilder.append("Bold text indicates class or relationship to remove to decompose cycle");
+ stringBuilder.append(
+ "* indicates class to remove, bold text indicates relationships to remove to decompose cycle");
stringBuilder.append(" ");
int classCount = cycle.getCycleNodes().size();
int relationshipCount = cycle.getEdgeSet().size();
@@ -561,9 +618,11 @@ private String renderSingleCycle(RankedCycle cycle) {
for (String vertex : cycle.getVertexSet()) {
stringBuilder.append("
");
- String className = getClassName(vertex);
+ String className;
if (vertexesToRemove.contains(vertex)) {
- className = "" + className + " ";
+ className = hyperlinkClass(vertex, repoUrl, codebaseGraphDTO) + "* ";
+ } else {
+ className = hyperlinkClass(vertex, repoUrl, codebaseGraphDTO);
}
stringBuilder.append(drawTableCell(className));
@@ -574,9 +633,6 @@ private String renderSingleCycle(RankedCycle cycle) {
if (edgesToRemove.contains(edge)) {
edges.append("");
edges.append(renderEdge(edge));
- if (cycle.getMinCutEdges().contains(edge)) {
- edges.append("*");
- }
edges.append(" ");
} else {
edges.append(renderEdge(edge));
@@ -596,11 +652,11 @@ private String renderSingleCycle(RankedCycle cycle) {
return stringBuilder.toString();
}
- public String renderClassGraphVisuals() {
+ public String renderClassGraphVisuals(String repoUrl, CodebaseGraphDTO codebaseGraphDTO) {
return ""; // empty on purpose
}
- public String renderCycleVisuals(RankedCycle cycle) {
+ public String renderCycleVisuals(RankedCycle cycle, String repoUrl, CodebaseGraphDTO codebaseGraphDTO) {
return ""; // empty on purpose
}
@@ -682,11 +738,16 @@ String getOutputName() {
}
/**
- * Renders a table section for any non-God-Class disharmony type.
+ * Renders a table section for any disharmony type.
* Column headers are derived from the ranked metrics carried on each RankedDisharmony.
*/
public String renderDisharmonyInfo(
- String anchorId, String title, boolean methodLevel, boolean showDetails, List ranked) {
+ String repoUrl,
+ String anchorId,
+ String title,
+ boolean methodLevel,
+ boolean showDetails,
+ List ranked) {
if (ranked.isEmpty()) {
return "";
}
@@ -709,8 +770,7 @@ public String renderDisharmonyInfo(
sb.append("\n");
// Build headers from the first item's ranked metrics
- List sampleMetrics =
- ranked.get(0).getRankedMetrics();
+ List sampleMetrics = ranked.get(0).getRankedMetrics();
boolean showPartners = ranked.get(0).getDuplicationPartners() != null;
@@ -727,7 +787,7 @@ public String renderDisharmonyInfo(
sb.append("Change Proneness Rank \n");
sb.append("Effort Rank \n");
if (showDetails) {
- for (org.hjug.graphbuilder.metrics.DisharmonyMetric m : sampleMetrics) {
+ for (DisharmonyMetric m : sampleMetrics) {
sb.append("").append(m.getName()).append(" \n");
sb.append("").append(m.getName()).append(" Rank \n");
}
@@ -746,7 +806,8 @@ public String renderDisharmonyInfo(
sb.append("\n");
for (RankedDisharmony rd : ranked) {
sb.append("\n");
- sb.append(drawTableCell(rd.getFileName()));
+ sb.append(drawTableCell(
+ "" + rd.getFileName() + " "));
if (methodLevel) {
String sig = rd.getMethodSignature();
if (!showDetails && sig != null) {
@@ -763,7 +824,7 @@ public String renderDisharmonyInfo(
sb.append(drawTableCell(rd.getChangePronenessRank().toString()));
sb.append(drawTableCell(rd.getEffortRank().toString()));
if (showDetails) {
- for (org.hjug.graphbuilder.metrics.DisharmonyMetric m : rd.getRankedMetrics()) {
+ for (DisharmonyMetric m : rd.getRankedMetrics()) {
double v = m.getValue();
String formatted = (v == Math.floor(v)) ? String.valueOf((long) v) : String.valueOf(v);
sb.append(drawTableCell(formatted));
diff --git a/report/src/test/java/org/hjug/refactorfirst/report/DisharmonyRenderingTest.java b/report/src/test/java/org/hjug/refactorfirst/report/DisharmonyRenderingTest.java
index 67892887..b3e64bc0 100644
--- a/report/src/test/java/org/hjug/refactorfirst/report/DisharmonyRenderingTest.java
+++ b/report/src/test/java/org/hjug/refactorfirst/report/DisharmonyRenderingTest.java
@@ -21,7 +21,7 @@ class DisharmonyRenderingTest {
void renderDisharmonyInfoContainsTitle() {
List ranked = List.of(makeRankedDisharmony("BrainClass.java", null, 1, 57.0, 3.0, 0.3));
- String html = simpleReport.renderDisharmonyInfo("BRAIN", "Brain Classes", false, false, ranked);
+ String html = simpleReport.renderDisharmonyInfo("", "BRAIN", "Brain Classes", false, false, ranked);
assertTrue(html.contains("Brain Classes"), "HTML must contain the section title");
assertTrue(html.contains("id=\"BRAIN\""), "HTML must contain the anchor id");
@@ -32,7 +32,7 @@ void simpleModeShowsDescriptionColumnNotMetricColumns() {
List ranked = List.of(makeRankedDisharmony("BrainClass.java", null, 1, 57.0, 3.0, 0.3));
ranked.get(0).setDescription("Brain Class detected: Brain Methods=1, LOC=200, WMC=3, TCC=0.3");
- String html = simpleReport.renderDisharmonyInfo("BRAIN", "Brain Classes", false, false, ranked);
+ String html = simpleReport.renderDisharmonyInfo("", "BRAIN", "Brain Classes", false, false, ranked);
assertTrue(html.contains(" ranked = List.of(makeRankedDisharmony("BrainClass.java", null, 1, 57.0, 3.0, 0.3));
- String simple = simpleReport.renderDisharmonyInfo("BRAIN", "Brain Classes", false, false, ranked);
- String detailed = simpleReport.renderDisharmonyInfo("BRAIN", "Brain Classes", false, true, ranked);
+ String simple = simpleReport.renderDisharmonyInfo("", "BRAIN", "Brain Classes", false, false, ranked);
+ String detailed = simpleReport.renderDisharmonyInfo("", "BRAIN", "Brain Classes", false, true, ranked);
assertFalse(simple.contains("BrainMethods Rank"), "Simple mode should not show rank columns");
assertFalse(simple.contains("BrainMethods "), "Simple mode should not show metric value columns");
@@ -60,7 +60,7 @@ void renderDisharmonyInfoForMethodLevelShowsMethodColumn() {
List ranked =
List.of(makeRankedDisharmony("BrainClass.java", "heavyMethod()", 1, 70.0, 5.0, 5.0));
- String html = simpleReport.renderDisharmonyInfo("BRAIN_METHOD", "Brain Methods", true, false, ranked);
+ String html = simpleReport.renderDisharmonyInfo("", "BRAIN_METHOD", "Brain Methods", true, false, ranked);
assertTrue(html.contains("Method"), "Method-level rendering must include a Method column header");
assertTrue(html.contains("heavyMethod()"), "Method-level rendering must include the method signature");
@@ -70,7 +70,7 @@ void renderDisharmonyInfoForMethodLevelShowsMethodColumn() {
void renderDisharmonyInfoForClassLevelDoesNotShowMethodColumn() {
List ranked = List.of(makeRankedDisharmony("BrainClass.java", null, 1, 57.0, 3.0, 0.3));
- String html = simpleReport.renderDisharmonyInfo("BRAIN", "Brain Classes", false, false, ranked);
+ String html = simpleReport.renderDisharmonyInfo("", "BRAIN", "Brain Classes", false, false, ranked);
// Class-level should not have an empty method cell (null signature)
assertFalse(html.contains("null"), "Class-level rendering must not have null method signature cells");
@@ -120,7 +120,7 @@ void significantDuplicationTableShowsDuplicatePartnersColumn() {
rd.setDuplicationPartners("computeResult(int) ↔ CrossClassB.computeResult(int)");
String html =
- simpleReport.renderDisharmonyInfo("SIG_DUP", "Significant Duplication", false, false, List.of(rd));
+ simpleReport.renderDisharmonyInfo("", "SIG_DUP", "Significant Duplication", false, false, List.of(rd));
assertTrue(html.contains("Duplicate Partners"), "Table must show 'Duplicate Partners' column header");
assertTrue(html.contains("CrossClassB"), "Table must show partner class name in the Duplicate Partners cell");
@@ -130,7 +130,7 @@ void significantDuplicationTableShowsDuplicatePartnersColumn() {
void otherDisharmonyTableOmitsDuplicatePartnersColumn() {
RankedDisharmony rd = makeRankedDisharmony("BrainClass.java", null, 1, 57.0, 3.0, 0.3);
- String html = simpleReport.renderDisharmonyInfo("BRAIN", "Brain Classes", false, false, List.of(rd));
+ String html = simpleReport.renderDisharmonyInfo("", "BRAIN", "Brain Classes", false, false, List.of(rd));
assertFalse(html.contains("Duplicate Partners"), "Non-duplication table must not show 'Duplicate Partners'");
}
diff --git a/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportTest.java b/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportTest.java
index 514e791b..585ff578 100644
--- a/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportTest.java
+++ b/report/src/test/java/org/hjug/refactorfirst/report/HtmlReportTest.java
@@ -1,10 +1,13 @@
package org.hjug.refactorfirst.report;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import java.util.*;
import org.hjug.cbc.CycleNode;
import org.hjug.cbc.RankedCycle;
+import org.hjug.graphbuilder.CodebaseGraphDTO;
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultDirectedWeightedGraph;
import org.jgrapht.graph.DefaultWeightedEdge;
@@ -52,15 +55,21 @@ void buildCycleDot() {
new RankedCycle(cycleName, 0, classGraph.vertexSet(), classGraph.edgeSet(), 0, null, cycleNodes);
HtmlReport htmlReport = new HtmlReport();
- String dot = htmlReport.buildCycleDot(classGraph, rankedCycle);
-
+ CodebaseGraphDTO dto = mock(CodebaseGraphDTO.class);
+ HashMap map = new HashMap<>();
+ map.put("A", "/src/main/java/org/hjug/refactorfirst/A.java");
+ map.put("B", "/src/main/java/org/hjug/refactorfirst/B.java");
+ map.put("C", "/src/main/java/org/hjug/refactorfirst/C.java");
+ when(dto.getClassToSourceFilePathMapping()).thenReturn(map);
+ String repoUrl = "https://github.com/refactorfirst/RefactorFirst/blob";
+ String dot = htmlReport.buildCycleDot(classGraph, rankedCycle, repoUrl, dto);
String expectedDot = "`strict digraph G {\n"
+ "A -> B [ label = \"2\" weight = \"2\" ];\n"
+ "B -> C [ label = \"1\" weight = \"1\" ];\n"
+ "C -> A [ label = \"1\" weight = \"1\" ];\n"
- + "A;\n"
- + "B;\n"
- + "C;\n"
+ + "A [URL=\"https://github.com/refactorfirst/RefactorFirst/blob/src/main/java/org/hjug/refactorfirst/A.java\" target=\"_blank\"];\n"
+ + "B [URL=\"https://github.com/refactorfirst/RefactorFirst/blob/src/main/java/org/hjug/refactorfirst/B.java\" target=\"_blank\"];\n"
+ + "C [URL=\"https://github.com/refactorfirst/RefactorFirst/blob/src/main/java/org/hjug/refactorfirst/C.java\" target=\"_blank\"];\n"
+ "}`;";
assertEquals(expectedDot, dot);
diff --git a/report/src/test/resources/dotPlayground.html b/report/src/test/resources/dotPlayground.html
new file mode 100644
index 00000000..2d57d335
--- /dev/null
+++ b/report/src/test/resources/dotPlayground.html
@@ -0,0 +1,73 @@
+
+
+
+
+ Graphviz DOT in HTML
+
+
+
+Graphviz DOT Embedded in HTML
+Enter DOT language code below and click "Render Graph".
+
+
+
+
+Render Graph
+
+
+
+
+
+
+
+
+
+
+