Skip to content

Commit 18e74fc

Browse files
svaningelgemSteven Van Ingelgem
andauthored
Cleaning up the tree (#11)
* Adding build caching * WIP * Cleaning up the printing * Clean up the tests * debugging linux vs windows * Fix the tearDown * Improving branch coverage * Setting the correct locale * Checking to increase coverage on Defaults --------- Co-authored-by: Steven Van Ingelgem <[email protected]>
1 parent c43acf7 commit 18e74fc

16 files changed

+838
-692
lines changed

jacoco-console-reporter/src/main/java/io/github/svaningelgem/CoverageMetrics.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.github.svaningelgem;
22

3+
import lombok.AllArgsConstructor;
34
import lombok.Data;
5+
import lombok.NoArgsConstructor;
46
import org.jetbrains.annotations.NotNull;
57

68
/**
@@ -9,7 +11,9 @@
911
* All metrics maintain both total count and covered count for percentage calculation.
1012
*/
1113
@Data
12-
class CoverageMetrics {
14+
@AllArgsConstructor
15+
@NoArgsConstructor
16+
public class CoverageMetrics implements Cloneable {
1317
/**
1418
* Total number of classes in the scope
1519
*/
@@ -53,4 +57,9 @@ void add(@NotNull CoverageMetrics other) {
5357
totalBranches += other.totalBranches;
5458
coveredBranches += other.coveredBranches;
5559
}
60+
61+
@Override
62+
public CoverageMetrics clone() {
63+
return new CoverageMetrics(totalClasses, coveredClasses, totalMethods, coveredMethods, totalLines, coveredLines, totalBranches, coveredBranches);
64+
}
5665
}

jacoco-console-reporter/src/main/java/io/github/svaningelgem/Defaults.java

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,19 @@ public class Defaults {
99
static final int METRICS_WIDTH = 20;
1010

1111
// Define tree characters based on terminal capabilities
12-
public static final String LASTDIR_SPACE = " ";
13-
public static final String VERTICAL_LINE = "│ ";
14-
public static final String TEE = "├─";
15-
public static final String CORNER = "└─";
12+
static final String LAST_DIR_SPACE = " ";
13+
static final String VERTICAL_LINE = "│ ";
14+
static final String TEE = "├─";
15+
static final String CORNER = "└─";
1616

1717
static final String DIVIDER = getDivider();
18-
static final String HEADER_FORMAT = "%-" + PACKAGE_WIDTH + "s " + VERTICAL_LINE + "%-" + METRICS_WIDTH + "s " + VERTICAL_LINE + "%-" + METRICS_WIDTH + "s " + VERTICAL_LINE + "%-" + METRICS_WIDTH + "s " + VERTICAL_LINE + "%-" + METRICS_WIDTH + "s";
1918
static final String LINE_FORMAT = "%-" + PACKAGE_WIDTH + "s " + VERTICAL_LINE + "%-" + METRICS_WIDTH + "s " + VERTICAL_LINE + "%-" + METRICS_WIDTH + "s " + VERTICAL_LINE + "%-" + METRICS_WIDTH + "s " + VERTICAL_LINE + "%-" + METRICS_WIDTH + "s";
2019

2120
/**
2221
* Truncates a string in the middle if it exceeds maxLength
2322
* Example: "com.example.very.long.package.name" -> "com.example...kage.name"
2423
*/
25-
public static @NotNull String truncateMiddle(@NotNull String input) {
24+
static @NotNull String truncateMiddle(@NotNull String input) {
2625
if (input.length() <= PACKAGE_WIDTH) {
2726
return input;
2827
}
@@ -42,27 +41,18 @@ public class Defaults {
4241
* @return Formatted string showing percentage and ratio (e.g., "75.00% (3/4)")
4342
*/
4443
@Contract(pure = true)
45-
public static @NotNull String formatCoverage(int covered, int total) {
46-
if (total == 0) return "100.00% (0/0)";
47-
double percentage = (double) covered / total * 100;
48-
return String.format("%5.2f%% (%d/%d)", percentage, covered, total);
44+
static @NotNull String formatCoverage(double covered, double total) {
45+
if (total <= 0) return " ***** (0/0)";
46+
double percentage = covered / total * 100;
47+
return String.format("%5.2f%% (%d/%d)", percentage, (int)covered, (int)total);
4948
}
5049

5150
/**
5251
* Build a divider with certain widths
5352
*/
5453
private static @NotNull String getDivider() {
55-
StringBuilder divider = new StringBuilder();
56-
for (int i = 0; i < PACKAGE_WIDTH; i++) divider.append("-");
57-
divider.append("-|-");
58-
for (int i = 0; i < METRICS_WIDTH; i++) divider.append("-");
59-
divider.append("-|-");
60-
for (int i = 0; i < METRICS_WIDTH; i++) divider.append("-");
61-
divider.append("-|-");
62-
for (int i = 0; i < METRICS_WIDTH; i++) divider.append("-");
63-
divider.append("-|-");
64-
for (int i = 0; i < METRICS_WIDTH; i++) divider.append("-");
65-
return divider.toString();
54+
return String.format(Defaults.LINE_FORMAT, "", "", "", "", "").replace(' ', '-');
6655
}
6756

57+
private Defaults() { }
6858
}

jacoco-console-reporter/src/main/java/io/github/svaningelgem/DirectoryNode.java

Lines changed: 48 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
import org.jetbrains.annotations.NotNull;
66

77
import java.util.*;
8+
import java.util.stream.Collectors;
89

910
/**
1011
* A node representing a directory (package) in the coverage tree.
1112
*/
1213
@Data
1314
@RequiredArgsConstructor
14-
class DirectoryNode implements FileSystemNode {
15+
public class DirectoryNode implements FileSystemNode {
1516
/**
1617
* Name of the directory/package component
1718
*/
@@ -29,235 +30,77 @@ class DirectoryNode implements FileSystemNode {
2930

3031
@Override
3132
public CoverageMetrics getMetrics() {
32-
return aggregateMetrics();
33-
}
34-
35-
/**
36-
* Aggregate metrics from this directory's files and subdirectories
37-
*/
38-
CoverageMetrics aggregateMetrics() {
3933
CoverageMetrics aggregated = new CoverageMetrics();
4034
sourceFiles.forEach(file -> aggregated.add(file.getMetrics()));
41-
subdirectories.values().forEach(subdir -> aggregated.add(subdir.aggregateMetrics()));
35+
subdirectories.values().forEach(subdir -> aggregated.add(subdir.getMetrics()));
4236
return aggregated;
4337
}
4438

45-
@Override
46-
public boolean shouldInclude(boolean showFiles) {
47-
// Check if this directory has any files (when showing files)
48-
if (showFiles && !sourceFiles.isEmpty()) {
49-
return true;
50-
}
51-
52-
// Check if it has any subdirectories that should be included
53-
for (DirectoryNode subdir : subdirectories.values()) {
54-
if (subdir.shouldInclude(showFiles)) {
55-
return true;
56-
}
57-
}
58-
59-
// Empty directory - skip it
60-
return false;
39+
public boolean shouldInclude() {
40+
return !sourceFiles.isEmpty() || subdirectories.values().stream().anyMatch(DirectoryNode::shouldInclude);
6141
}
6242

63-
@Override
64-
public void printTree(org.apache.maven.plugin.logging.Log log, String prefix,
65-
String format, String packagePath, boolean showFiles) {
66-
// Skip empty directories
67-
if (!shouldInclude(showFiles)) {
68-
return;
69-
}
43+
private <T extends FileSystemNode> void printNodes(org.apache.maven.plugin.logging.Log log, String prefix,
44+
String format, String packagePath, boolean showFiles, @NotNull List<T> nodes, boolean extraCheck) {
45+
for (int i = 0; i < nodes.size(); i++) {
46+
boolean isLast = (i == nodes.size() - 1) && extraCheck;
47+
FileSystemNode node = nodes.get(i);
7048

71-
// Skip printing the empty root node
72-
boolean isRoot = name.isEmpty();
73-
String currentPath = isRoot ? packagePath :
74-
(packagePath.isEmpty() ? name : packagePath + "." + name);
75-
76-
if (!isRoot) {
77-
log.info(String.format(format,
78-
Defaults.truncateMiddle(prefix + name),
79-
Defaults.formatCoverage(getMetrics().getCoveredClasses(), getMetrics().getTotalClasses()),
80-
Defaults.formatCoverage(getMetrics().getCoveredMethods(), getMetrics().getTotalMethods()),
81-
Defaults.formatCoverage(getMetrics().getCoveredBranches(), getMetrics().getTotalBranches()),
82-
Defaults.formatCoverage(getMetrics().getCoveredLines(), getMetrics().getTotalLines())));
49+
node.printTree(log, determineNewPrefix(prefix, isLast), format, packagePath, showFiles);
8350
}
51+
}
8452

85-
// Collect directory nodes and file nodes separately
86-
List<DirectoryNode> dirNodes = new ArrayList<>();
87-
List<SourceFileNode> fileNodes = new ArrayList<>();
53+
private @NotNull String determineNewPrefix(@NotNull String oldPrefix, boolean isLast) {
54+
String prefix = oldPrefix;
8855

89-
// Add subdirectories
90-
for (DirectoryNode subdir : subdirectories.values()) {
91-
if (subdir.shouldInclude(showFiles)) {
92-
dirNodes.add(subdir);
93-
}
56+
if (prefix.endsWith(Defaults.CORNER)) {
57+
prefix = prefix.substring(0, prefix.length() - Defaults.CORNER.length()) + Defaults.LAST_DIR_SPACE;
9458
}
95-
96-
// Add source files if needed
97-
if (showFiles) {
98-
fileNodes.addAll(sourceFiles);
99-
}
100-
101-
// Sort nodes
102-
Collections.sort(dirNodes);
103-
Collections.sort(fileNodes);
104-
105-
// Determine if we need tree indicators at the first level
106-
boolean useTreeForRoot = dirNodes.size() > 1 || !fileNodes.isEmpty();
107-
108-
// Print directory nodes first
109-
for (int i = 0; i < dirNodes.size(); i++) {
110-
boolean isLast = (i == dirNodes.size() - 1) && fileNodes.isEmpty();
111-
DirectoryNode node = dirNodes.get(i);
112-
113-
if (isRoot) {
114-
// Handle collapsible directories at root level
115-
if (shouldCollapseDirectory(node, showFiles)) {
116-
String displayPrefix = useTreeForRoot ? (isLast ? Defaults.CORNER : Defaults.TEE) : "";
117-
printCollapsedPath(log, node, displayPrefix, isLast, format, currentPath,
118-
showFiles, useTreeForRoot);
119-
} else {
120-
// Normal node at root level
121-
String rootPrefix = useTreeForRoot ? (isLast ? Defaults.CORNER : Defaults.TEE) : "";
122-
node.printTree(log, rootPrefix, format, currentPath, showFiles);
123-
}
124-
} else {
125-
// Non-root nodes
126-
String connector = isLast ? Defaults.CORNER : Defaults.TEE;
127-
128-
// Handle collapsible directories
129-
if (shouldCollapseDirectory(node, showFiles)) {
130-
printCollapsedPath(log, node, prefix, isLast, format, currentPath, showFiles, true);
131-
} else {
132-
// Print the node normally
133-
node.printTree(log, prefix + connector, format, currentPath, showFiles);
134-
}
135-
}
59+
else if (prefix.endsWith(Defaults.TEE)) {
60+
prefix = prefix.substring(0, prefix.length() - Defaults.TEE.length()) + Defaults.VERTICAL_LINE;
13661
}
13762

138-
// Print source files after directories
139-
if (!fileNodes.isEmpty()) {
140-
// Calculate the prefix for files
141-
String filePrefix = isRoot ? "" : prefix.replace(Defaults.TEE, Defaults.VERTICAL_LINE).replace(Defaults.CORNER, Defaults.LASTDIR_SPACE);
142-
143-
for (int i = 0; i < fileNodes.size(); i++) {
144-
boolean isLast = (i == fileNodes.size() - 1);
145-
SourceFileNode node = fileNodes.get(i);
146-
147-
String connector = isLast ? Defaults.CORNER : Defaults.TEE;
148-
if (isRoot && useTreeForRoot) {
149-
node.printTree(log, connector, format, currentPath, showFiles);
150-
} else {
151-
node.printTree(log, filePrefix + connector, format, currentPath, showFiles);
152-
}
153-
}
154-
}
63+
String connector = isLast ? Defaults.CORNER : Defaults.TEE;
64+
return prefix + connector;
15565
}
15666

157-
/**
158-
* Determines if a directory should be collapsed with its children
159-
* (i.e., it has exactly one subdirectory and no files)
160-
*/
161-
private boolean shouldCollapseDirectory(DirectoryNode dir, boolean showFiles) {
162-
if (showFiles && !dir.getSourceFiles().isEmpty()) {
163-
return false;
67+
@Override
68+
public void printTree(org.apache.maven.plugin.logging.@NotNull Log log, String prefix,
69+
String format, String packagePath, boolean showFiles) {
70+
// Skip empty directories
71+
if (!shouldInclude()) {
72+
return;
16473
}
16574

166-
if (dir.getSubdirectories().size() != 1) {
167-
return false;
168-
}
75+
packagePath = packagePath.replaceAll("^\\.", ""); // ltrim('.')
16976

170-
DirectoryNode subdir = dir.getSubdirectories().values().iterator().next();
171-
return subdir.shouldInclude(showFiles);
172-
}
173-
174-
/**
175-
* Print a collapsed directory path (e.g., "com.example" instead of "com" -> "example")
176-
*/
177-
private void printCollapsedPath(org.apache.maven.plugin.logging.Log log, @NotNull DirectoryNode dir,
178-
String prefix, boolean isLast, String format,
179-
String packagePath, boolean showFiles, boolean useTreeIndicator) {
180-
// Build the collapsed path string
181-
StringBuilder path = new StringBuilder(dir.getName());
182-
DirectoryNode current = dir;
77+
// Skip printing the empty root node
78+
final boolean isRoot = name.isEmpty();
18379

184-
// Follow the chain of single subdirectories
185-
while (shouldCollapseDirectory(current, showFiles)) {
186-
DirectoryNode subdir = current.getSubdirectories().values().iterator().next();
187-
path.append(".").append(subdir.getName());
188-
current = subdir;
189-
}
80+
// Collect directory nodes and file nodes separately
81+
List<DirectoryNode> dirNodes = subdirectories.values().stream().filter(DirectoryNode::shouldInclude).sorted().collect(Collectors.toList());
82+
List<SourceFileNode> fileNodes = showFiles ? sourceFiles.stream().sorted().collect(Collectors.toList()) : new ArrayList<>();
19083

191-
// Display the collapsed path as a node
192-
CoverageMetrics metrics = dir.getMetrics();
193-
String displayPath;
194-
if (useTreeIndicator) {
195-
displayPath = prefix + path.toString();
196-
} else {
197-
displayPath = path.toString();
84+
boolean shouldCollapse = dirNodes.size() == 1 && fileNodes.isEmpty();
85+
if (shouldCollapse) {
86+
DirectoryNode onlyNode = dirNodes.get(0);
87+
onlyNode.printTree(log, prefix, format, packagePath + "." + getName(), showFiles);
88+
return;
19889
}
19990

91+
String printableName = isRoot ? "<root>" : prefix + packagePath + (packagePath.isEmpty() ? "" : ".") + name;
20092
log.info(String.format(format,
201-
Defaults.truncateMiddle(displayPath),
202-
Defaults.formatCoverage(metrics.getCoveredClasses(), metrics.getTotalClasses()),
203-
Defaults.formatCoverage(metrics.getCoveredMethods(), metrics.getTotalMethods()),
204-
Defaults.formatCoverage(metrics.getCoveredBranches(), metrics.getTotalBranches()),
205-
Defaults.formatCoverage(metrics.getCoveredLines(), metrics.getTotalLines())));
93+
Defaults.truncateMiddle(printableName),
94+
Defaults.formatCoverage(getMetrics().getCoveredClasses(), getMetrics().getTotalClasses()),
95+
Defaults.formatCoverage(getMetrics().getCoveredMethods(), getMetrics().getTotalMethods()),
96+
Defaults.formatCoverage(getMetrics().getCoveredBranches(), getMetrics().getTotalBranches()),
97+
Defaults.formatCoverage(getMetrics().getCoveredLines(), getMetrics().getTotalLines())));
20698

207-
// Calculate the full package path for children
208-
String fullPath = packagePath.isEmpty() ? path.toString() :
209-
packagePath + "." + path.toString();
99+
packagePath = ""; // Reset because we shouldn't collapse now anymore
210100

211-
// Separate directories and files for consistent ordering
212-
List<DirectoryNode> childDirs = new ArrayList<>();
213-
List<SourceFileNode> childFiles = new ArrayList<>();
214-
215-
// Get the contents of the last directory in the chain
216-
for (DirectoryNode subdir : current.getSubdirectories().values()) {
217-
if (subdir.shouldInclude(showFiles)) {
218-
childDirs.add(subdir);
219-
}
220-
}
221-
222-
if (showFiles) {
223-
childFiles.addAll(current.getSourceFiles());
224-
}
225-
226-
// Sort the child nodes
227-
Collections.sort(childDirs);
228-
Collections.sort(childFiles);
229-
230-
// Calculate the base prefix for children
231-
String basePrefixForChildren;
232-
if (useTreeIndicator) {
233-
basePrefixForChildren = isLast ? Defaults.LASTDIR_SPACE : Defaults.VERTICAL_LINE;
234-
} else {
235-
basePrefixForChildren = " "; // No tree indicator at root level
236-
}
237-
238-
// Print directories first
239-
for (int i = 0; i < childDirs.size(); i++) {
240-
boolean isLastDir = (i == childDirs.size() - 1) && childFiles.isEmpty();
241-
DirectoryNode node = childDirs.get(i);
242-
String childConnector = isLastDir ? Defaults.CORNER : Defaults.TEE;
243-
244-
if (shouldCollapseDirectory(node, showFiles)) {
245-
printCollapsedPath(log, node, basePrefixForChildren, isLastDir, format,
246-
fullPath, showFiles, true);
247-
} else {
248-
node.printTree(log, basePrefixForChildren + childConnector, format,
249-
fullPath, showFiles);
250-
}
251-
}
252-
253-
// Print files after directories
254-
for (int i = 0; i < childFiles.size(); i++) {
255-
boolean isLastFile = (i == childFiles.size() - 1);
256-
SourceFileNode node = childFiles.get(i);
257-
String childConnector = isLastFile ? Defaults.CORNER : Defaults.TEE;
258-
259-
node.printTree(log, basePrefixForChildren + childConnector, format,
260-
fullPath, showFiles);
261-
}
101+
// Print directory nodes first
102+
printNodes(log, prefix, format, packagePath, showFiles, dirNodes, fileNodes.isEmpty());
103+
// Then files
104+
printNodes(log, prefix, format, packagePath, showFiles, fileNodes, true);
262105
}
263106
}

0 commit comments

Comments
 (0)