diff --git a/src/main/java/io/github/finoid/maven/plugins/codequality/report/AbstractConsoleViolationReporter.java b/src/main/java/io/github/finoid/maven/plugins/codequality/report/AbstractConsoleViolationReporter.java
new file mode 100644
index 0000000..d843988
--- /dev/null
+++ b/src/main/java/io/github/finoid/maven/plugins/codequality/report/AbstractConsoleViolationReporter.java
@@ -0,0 +1,88 @@
+package io.github.finoid.maven.plugins.codequality.report;
+
+import io.github.finoid.maven.plugins.codequality.ExecutionContext;
+import io.github.finoid.maven.plugins.codequality.filter.Violations;
+import org.apache.maven.plugin.logging.Log;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A base for console-based {@link ViolationReporter} implementations.
+ *
+ *
Handles what every console reporter does the same way: splitting the violations into permissive and
+ * non-permissive groups, emitting the header of each group, and choosing the log level the group is reported at.
+ * Subclasses decide how the violations themselves are rendered, through {@link #logViolations}.
+ */
+abstract class AbstractConsoleViolationReporter implements ViolationReporter {
+ protected static final String GREEN = "\u001B[32m";
+ protected static final String YELLOW = "\u001B[33m";
+ protected static final String RESET = "\u001B[0m";
+
+ /**
+ * Reports the collected violations by grouping them into permissive and non-permissive categories,
+ * and logs each group with formatting and category-based log levels.
+ *
+ * @param context the context of the current mojo execution
+ * @param violations the results of executed code analysis steps containing violations
+ */
+ @Override
+ public void report(final ExecutionContext context, final Violations violations) {
+ final Log log = context.getLog();
+
+ logViolationsForType(log, violations.getPermissiveViolations(), PermissiveType.PERMISSIVE);
+ logViolationsForType(log, violations.getNonPermissiveViolations(), PermissiveType.NON_PERMISSIVE);
+ }
+
+ /**
+ * Renders and logs a group of violations. The header of the group has already been logged.
+ *
+ * @param log the log of the current mojo execution
+ * @param violations the violations of the group, never empty
+ * @param permissiveType the category the violations belong to
+ */
+ protected abstract void logViolations(final Log log, final List violations, final PermissiveType permissiveType);
+
+ /**
+ * Logs the message as a warning for non-permissive violations, and as info otherwise.
+ *
+ * @param log the log of the current mojo execution
+ * @param permissiveType the category the message relates to
+ * @param message the message to log
+ */
+ protected static void logWithLevel(final Log log, final PermissiveType permissiveType, final String message) {
+ if (permissiveType == PermissiveType.NON_PERMISSIVE) {
+ log.warn(message);
+ } else {
+ log.info(message);
+ }
+ }
+
+ private void logViolationsForType(final Log log, final List violations, final PermissiveType permissiveType) {
+ if (violations.isEmpty()) {
+ log.info(String.format("✅ %s ##### No %s violations found ##### %s ✅ ", GREEN, permissiveType.displayName(), RESET));
+ return;
+ }
+
+ final String message = String.format("%s ##### found %d %s violations ##### %s",
+ (permissiveType == PermissiveType.NON_PERMISSIVE) ? YELLOW : GREEN,
+ violations.size(),
+ permissiveType.displayName(),
+ RESET
+ );
+
+ logWithLevel(log, permissiveType, (permissiveType == PermissiveType.NON_PERMISSIVE ? "⚠ " : "✅ ") + message);
+
+ logViolations(log, violations, permissiveType);
+ }
+
+ protected enum PermissiveType {
+ PERMISSIVE,
+ NON_PERMISSIVE;
+
+ String displayName() {
+ return name().replace('_', ' ')
+ .toLowerCase(Locale.ROOT);
+ }
+ }
+}
diff --git a/src/main/java/io/github/finoid/maven/plugins/codequality/report/ConsolePlainViolationReporter.java b/src/main/java/io/github/finoid/maven/plugins/codequality/report/ConsolePlainViolationReporter.java
index be66dc9..5fd6dd4 100644
--- a/src/main/java/io/github/finoid/maven/plugins/codequality/report/ConsolePlainViolationReporter.java
+++ b/src/main/java/io/github/finoid/maven/plugins/codequality/report/ConsolePlainViolationReporter.java
@@ -1,7 +1,5 @@
package io.github.finoid.maven.plugins.codequality.report;
-import io.github.finoid.maven.plugins.codequality.ExecutionContext;
-import io.github.finoid.maven.plugins.codequality.filter.Violations;
import io.github.finoid.maven.plugins.codequality.log.ViolationLinkableConsoleLogger;
import io.github.finoid.maven.plugins.codequality.util.Precondition;
import org.apache.maven.plugin.logging.Log;
@@ -10,7 +8,6 @@
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.List;
-import java.util.Locale;
/**
* A plain console-based implementation of {@link ViolationReporter} that logs code quality violations
@@ -21,13 +18,9 @@
*/
@Named("console-plain")
@Singleton
-public class ConsolePlainViolationReporter implements ViolationReporter {
+public class ConsolePlainViolationReporter extends AbstractConsoleViolationReporter {
public static final String NAME = "CONSOLE_PLAIN";
- private static final String GREEN = "\u001B[32m";
- private static final String YELLOW = "\u001B[33m";
- private static final String RESET = "\u001B[0m";
-
private final ViolationLinkableConsoleLogger violationLinkableConsoleLogger;
@Inject
@@ -35,60 +28,13 @@ public ConsolePlainViolationReporter(final ViolationLinkableConsoleLogger violat
this.violationLinkableConsoleLogger = Precondition.nonNull(violationLinkableConsoleLogger, "ViolationLinkableConsoleLogger shouldn't be null");
}
- /**
- * Reports all violations of at least {@link Severity#MINOR} level by grouping them
- * into permissive and non-permissive categories, and logs each group with formatting
- * and severity-based log levels.
- *
- * @param context the context of the current mojo execution
- * @param violations the results of executed code analysis steps containing violations
- */
- @Override
- public void report(final ExecutionContext context, final Violations violations) {
- final Log log = context.getLog();
-
- logViolationsForType(log, violations.getPermissiveViolations(), PermissiveType.PERMISSIVE);
- logViolationsForType(log, violations.getNonPermissiveViolations(), PermissiveType.NON_PERMISSIVE);
- }
-
@Override
public String name() {
return NAME;
}
- private void logViolationsForType(final Log log, final List violations, final PermissiveType permissiveType) {
- if (violations.isEmpty()) {
- log.info(String.format("✅ %s ##### No %s violations found ##### %s ✅ ", GREEN, permissiveType.displayName(), RESET));
- return;
- }
-
- final String message = String.format("%s ##### found %d %s violations ##### %s",
- (permissiveType == PermissiveType.NON_PERMISSIVE) ? YELLOW : GREEN,
- violations.size(),
- permissiveType.displayName(),
- RESET
- );
-
- logWithLevel(log, permissiveType, (permissiveType == PermissiveType.NON_PERMISSIVE ? "⚠ " : "✅ ") + message);
-
- violations.forEach(v -> logWithLevel(log, permissiveType, violationLinkableConsoleLogger.format(v)));
- }
-
- private static void logWithLevel(final Log log, final PermissiveType permissiveType, final String message) {
- if (permissiveType == PermissiveType.NON_PERMISSIVE) {
- log.warn(message);
- } else {
- log.info(message);
- }
- }
-
- private enum PermissiveType {
- PERMISSIVE,
- NON_PERMISSIVE;
-
- private String displayName() {
- return name().replace('_', ' ')
- .toLowerCase(Locale.ROOT);
- }
+ @Override
+ protected void logViolations(final Log log, final List violations, final PermissiveType permissiveType) {
+ violations.forEach(it -> logWithLevel(log, permissiveType, violationLinkableConsoleLogger.format(it)));
}
}
diff --git a/src/main/java/io/github/finoid/maven/plugins/codequality/report/ConsoleTableViolationReporter.java b/src/main/java/io/github/finoid/maven/plugins/codequality/report/ConsoleTableViolationReporter.java
index 283b22c..097e4f6 100644
--- a/src/main/java/io/github/finoid/maven/plugins/codequality/report/ConsoleTableViolationReporter.java
+++ b/src/main/java/io/github/finoid/maven/plugins/codequality/report/ConsoleTableViolationReporter.java
@@ -4,14 +4,11 @@
import de.vandermeer.asciitable.CWC_LongestLine;
import de.vandermeer.asciithemes.TA_GridThemes;
import de.vandermeer.skb.interfaces.transformers.textformat.TextAlignment;
-import io.github.finoid.maven.plugins.codequality.ExecutionContext;
-import io.github.finoid.maven.plugins.codequality.filter.Violations;
import org.apache.maven.plugin.logging.Log;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.List;
-import java.util.Locale;
/**
* A table console-based implementation of {@link ViolationReporter} that logs code quality violations
@@ -22,35 +19,30 @@
*/
@Named("console-table")
@Singleton
-public class ConsoleTableViolationReporter implements ViolationReporter {
+public class ConsoleTableViolationReporter extends AbstractConsoleViolationReporter {
public static final String NAME = "CONSOLE_TABLE";
- private static final String GREEN = "\u001B[32m";
- private static final String YELLOW = "\u001B[33m";
- private static final String RESET = "\u001B[0m";
-
/**
- * Reports all violations of at least {@link Severity#MINOR} level by grouping them
- * into permissive and non-permissive categories, and logs each group with formatting
- * and severity-based log levels.
- *
- * @param context the context of the current mojo execution
- * @param violations the results of executed code analysis steps containing violations
+ * The width, in characters, the table is rendered at.
*/
- @Override
- public void report(final ExecutionContext context, final Violations violations) {
- final Log log = context.getLog();
+ private static final int TABLE_WIDTH = 200;
- logViolationsForType(log, violations.getPermissiveViolations(), PermissiveType.PERMISSIVE);
- logViolationsForType(log, violations.getNonPermissiveViolations(), PermissiveType.NON_PERMISSIVE);
- }
+ /**
+ * The padding, in characters, on either side of every cell.
+ */
+ private static final int CELL_PADDING = 1;
@Override
public String name() {
return NAME;
}
- private String renderTable(final List violations) {
+ @Override
+ protected void logViolations(final Log log, final List violations, final PermissiveType permissiveType) {
+ logWithLevel(log, permissiveType, System.lineSeparator() + renderTable(violations));
+ }
+
+ private static String renderTable(final List violations) {
final AsciiTable table = new AsciiTable();
// Add the header
@@ -60,8 +52,6 @@ private String renderTable(final List violations) {
// Add each individual violation as a row
violations.forEach(it -> {
- table.setPadding(1);
-
table.addRow(
it.getTool(),
it.getRule(),
@@ -71,56 +61,23 @@ private String renderTable(final List violations) {
table.addRule();
});
+ // Applies to the rows added so far, so it has to happen once every row is in place
+ table.setPadding(CELL_PADDING);
table.setTextAlignment(TextAlignment.LEFT);
- table.getContext().setGridTheme(TA_GridThemes.FULL);
+ table.getContext()
+ .setGridTheme(TA_GridThemes.FULL);
final CWC_LongestLine cwc = new CWC_LongestLine();
table.getRenderer()
.setCWC(cwc);
- // Override specific column width ratios (relative percentages)
- cwc.add(10, 15) // Type
+ // Override the minimum and maximum width of each column
+ cwc.add(10, 15) // Tool
.add(20, 20) // Rule
- .add(40, 60) // Description!)
+ .add(40, 60) // Description
.add(25, 50) // Path
- .add(12, 20); // Column number
-
- return table.render(200);
- }
-
- private void logViolationsForType(final Log log, final List violations, final PermissiveType permissiveType) {
- if (violations.isEmpty()) {
- log.info(String.format("✅ %s ##### No %s violations found ##### %s ✅ ", GREEN, permissiveType.displayName(), RESET));
- return;
- }
-
- final String message = String.format("%s ##### found %d %s violations ##### %s",
- (permissiveType == PermissiveType.NON_PERMISSIVE) ? YELLOW : GREEN,
- violations.size(),
- permissiveType.displayName(),
- RESET
- );
-
- logWithLevel(log, permissiveType, (permissiveType == PermissiveType.NON_PERMISSIVE ? "⚠ " : "✅ ") + message);
-
- log.info(System.lineSeparator() + renderTable(violations));
- }
-
- private static void logWithLevel(final Log log, final PermissiveType permissiveType, final String message) {
- if (permissiveType == PermissiveType.NON_PERMISSIVE) {
- log.warn(message);
- } else {
- log.info(message);
- }
- }
-
- private enum PermissiveType {
- PERMISSIVE,
- NON_PERMISSIVE;
+ .add(12, 20); // Line/Column number
- private String displayName() {
- return name().replace('_', ' ')
- .toLowerCase(Locale.ROOT);
- }
+ return table.render(TABLE_WIDTH);
}
}
diff --git a/src/test/java/io/github/finoid/maven/plugins/codequality/fixtures/RecordingLog.java b/src/test/java/io/github/finoid/maven/plugins/codequality/fixtures/RecordingLog.java
new file mode 100644
index 0000000..d97abf0
--- /dev/null
+++ b/src/test/java/io/github/finoid/maven/plugins/codequality/fixtures/RecordingLog.java
@@ -0,0 +1,125 @@
+package io.github.finoid.maven.plugins.codequality.fixtures;
+
+import org.apache.maven.plugin.logging.Log;
+import org.jspecify.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link Log} which records everything written to it, together with the level it was written at.
+ *
+ * Intended for asserting on console output: {@link #render()} returns the recorded log as a single
+ * readable string, which makes it suitable for snapshotting.
+ */
+public class RecordingLog implements Log {
+ private static final String ESCAPE = "\u001B";
+
+ private final List entries = new ArrayList<>();
+
+ /**
+ * The recorded entries, each prefixed with the level it was logged at.
+ *
+ * @return the recorded entries, in the order they were logged
+ */
+ public List entries() {
+ return List.copyOf(entries);
+ }
+
+ /**
+ * Renders the recorded entries as a single string.
+ *
+ * ANSI escapes are rendered as their literal, printable escape sequence, to keep the coloring of the
+ * output both visible and diffable rather than being swallowed by whatever reads the snapshot.
+ *
+ * @return the recorded entries, separated by newlines
+ */
+ public String render() {
+ return String.join("\n", entries)
+ .replace(ESCAPE, "\\u001B");
+ }
+
+ private void record(final String level, @Nullable final CharSequence content) {
+ entries.add("[" + level + "] " + content);
+ }
+
+ @Override
+ public boolean isDebugEnabled() {
+ return true;
+ }
+
+ @Override
+ public void debug(@Nullable final CharSequence content) {
+ record("DEBUG", content);
+ }
+
+ @Override
+ public void debug(@Nullable final CharSequence content, @Nullable final Throwable error) {
+ record("DEBUG", content);
+ }
+
+ @Override
+ public void debug(@Nullable final Throwable error) {
+ record("DEBUG", String.valueOf(error));
+ }
+
+ @Override
+ public boolean isInfoEnabled() {
+ return true;
+ }
+
+ @Override
+ public void info(@Nullable final CharSequence content) {
+ record("INFO", content);
+ }
+
+ @Override
+ public void info(@Nullable final CharSequence content, @Nullable final Throwable error) {
+ record("INFO", content);
+ }
+
+ @Override
+ public void info(@Nullable final Throwable error) {
+ record("INFO", String.valueOf(error));
+ }
+
+ @Override
+ public boolean isWarnEnabled() {
+ return true;
+ }
+
+ @Override
+ public void warn(@Nullable final CharSequence content) {
+ record("WARN", content);
+ }
+
+ @Override
+ public void warn(@Nullable final CharSequence content, @Nullable final Throwable error) {
+ record("WARN", content);
+ }
+
+ @Override
+ public void warn(@Nullable final Throwable error) {
+ record("WARN", String.valueOf(error));
+ }
+
+ @Override
+ public boolean isErrorEnabled() {
+ return true;
+ }
+
+ @Override
+ public void error(@Nullable final CharSequence content) {
+ record("ERROR", content);
+ }
+
+ @Override
+ public void error(@Nullable final CharSequence content, @Nullable final Throwable error) {
+ record("ERROR", content);
+ }
+
+ @Override
+ public void error(@Nullable final Throwable error) {
+ record("ERROR", String.valueOf(error));
+ }
+}
diff --git a/src/test/java/io/github/finoid/maven/plugins/codequality/fixtures/UnitTest.java b/src/test/java/io/github/finoid/maven/plugins/codequality/fixtures/UnitTest.java
index e6a1359..9e2f7ae 100644
--- a/src/test/java/io/github/finoid/maven/plugins/codequality/fixtures/UnitTest.java
+++ b/src/test/java/io/github/finoid/maven/plugins/codequality/fixtures/UnitTest.java
@@ -2,6 +2,7 @@
import au.com.origin.snapshots.Expect;
import au.com.origin.snapshots.junit5.SnapshotExtension;
+import au.com.origin.snapshots.serializers.v1.ToStringSnapshotSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import io.github.finoid.maven.plugins.codequality.fixtures.snapshot.JsonSnapshotSerializer;
import org.junit.jupiter.api.Tag;
@@ -62,6 +63,22 @@ public void snapshot(final T toBeSnapshotted, final String... maskedFieldPat
.toMatchSnapshot(toBeSnapshotted);
}
+ /**
+ * Takes a snapshot of the string representation of the given object, verbatim.
+ *
+ * Unlike {@link #snapshot(Object)} the snapshot is not serialized as JSON, which keeps multi-line
+ * output - console output in particular - readable, and reviewable, in the snapshot file.
+ *
+ * @param toBeSnapshotted the object to be snapshotted
+ * @param the type of the object
+ */
+ @SuppressWarnings("SpellCheckingInspection")
+ public void snapshotText(final T toBeSnapshotted) {
+ expect
+ .serializer(new ToStringSnapshotSerializer())
+ .toMatchSnapshot(toBeSnapshotted);
+ }
+
/**
* Captures a snapshot of the given object within a specified test scenario.
* This method is useful when running multiple variations of the same test case
diff --git a/src/test/java/io/github/finoid/maven/plugins/codequality/report/ConsolePlainViolationReporterUnitTest.java b/src/test/java/io/github/finoid/maven/plugins/codequality/report/ConsolePlainViolationReporterUnitTest.java
new file mode 100644
index 0000000..fc2860b
--- /dev/null
+++ b/src/test/java/io/github/finoid/maven/plugins/codequality/report/ConsolePlainViolationReporterUnitTest.java
@@ -0,0 +1,89 @@
+package io.github.finoid.maven.plugins.codequality.report;
+
+import io.github.finoid.maven.plugins.codequality.ExecutionContext;
+import io.github.finoid.maven.plugins.codequality.filter.Violations;
+import io.github.finoid.maven.plugins.codequality.fixtures.RecordingLog;
+import io.github.finoid.maven.plugins.codequality.fixtures.UnitTest;
+import io.github.finoid.maven.plugins.codequality.log.ViolationLinkableConsoleLogger;
+import org.apache.maven.project.MavenProject;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+class ConsolePlainViolationReporterUnitTest extends UnitTest {
+ private final ConsolePlainViolationReporter unit = new ConsolePlainViolationReporter(new ViolationLinkableConsoleLogger());
+
+ private final RecordingLog log = new RecordingLog();
+
+ @Test
+ void givenNoViolations_whenReport_thenReportsBothCategoriesAsEmpty() {
+ unit.report(executionContext(), new Violations(List.of(), List.of()));
+
+ snapshotText(log.render());
+ }
+
+ @Test
+ void givenPermissiveViolations_whenReport_thenReportsEveryViolation() {
+ unit.report(executionContext(), new Violations(violations(), List.of()));
+
+ snapshotText(log.render());
+ }
+
+ @Test
+ void givenNonPermissiveViolations_whenReport_thenReportsEveryViolation() {
+ unit.report(executionContext(), new Violations(List.of(), violations()));
+
+ snapshotText(log.render());
+ }
+
+ @Test
+ void givenViolationsOfBothCategories_whenReport_thenReportsEachCategoryAtItsOwnLevel() {
+ unit.report(executionContext(), new Violations(violations(), violations()));
+
+ snapshotText(log.render());
+ }
+
+ /**
+ * Every non-permissive violation, and not only the header of the category, has to be reported as a warning.
+ * Reported as info they are dropped by a quiet build, leaving behind a warning without the violations it refers to.
+ */
+ @Test
+ void givenNonPermissiveViolations_whenReport_thenEveryViolationIsReportedAsWarning() {
+ unit.report(executionContext(), new Violations(List.of(), violations()));
+
+ final List entries = log.entries();
+
+ Assertions.assertEquals(violations().size() + 2, entries.size(),
+ "Expected a header per category, plus an entry per violation");
+
+ entries.stream()
+ .filter(it -> it.contains("file://"))
+ .forEach(it -> Assertions.assertTrue(it.startsWith("[WARN]"), () -> "Expected the violation to be reported as a warning: " + it));
+ }
+
+ private ExecutionContext executionContext() {
+ return ExecutionContext.of(new MavenProject(), log);
+ }
+
+ private static List violations() {
+ return List.of(
+ violation("checkstyle", "UnusedImports", "Unused import - java.util.Optional.", "src/main/java/Alpha.java", 3, 8),
+ violation("NullAway", "NullAway", "Passing @Nullable parameter 'name' where @NonNull is required.", "src/main/java/Gamma.java", 17, 24));
+ }
+
+ private static Violation violation(final String tool, final String rule, final String description, final String path,
+ final int line, final int columnNumber) {
+ return Violation.builder()
+ .tool(tool)
+ .rule(rule)
+ .description(description)
+ .fingerprint(tool + ":" + rule + ":" + path)
+ .severity(Severity.MAJOR)
+ .relativePath(path)
+ .fullPath("/workspace/" + path)
+ .line(line)
+ .columnNumber(columnNumber)
+ .build();
+ }
+}
diff --git a/src/test/java/io/github/finoid/maven/plugins/codequality/report/ConsoleTableViolationReporterUnitTest.java b/src/test/java/io/github/finoid/maven/plugins/codequality/report/ConsoleTableViolationReporterUnitTest.java
new file mode 100644
index 0000000..ec84b80
--- /dev/null
+++ b/src/test/java/io/github/finoid/maven/plugins/codequality/report/ConsoleTableViolationReporterUnitTest.java
@@ -0,0 +1,135 @@
+package io.github.finoid.maven.plugins.codequality.report;
+
+import io.github.finoid.maven.plugins.codequality.ExecutionContext;
+import io.github.finoid.maven.plugins.codequality.filter.Violations;
+import io.github.finoid.maven.plugins.codequality.fixtures.RecordingLog;
+import io.github.finoid.maven.plugins.codequality.fixtures.UnitTest;
+import org.apache.maven.project.MavenProject;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+class ConsoleTableViolationReporterUnitTest extends UnitTest {
+ private static final String COLUMN_DELIMITER = "│";
+ private static final String TABLE_TOP_LEFT_CORNER = "┌";
+
+ private final ConsoleTableViolationReporter unit = new ConsoleTableViolationReporter();
+
+ private final RecordingLog log = new RecordingLog();
+
+ @Test
+ void givenNoViolations_whenReport_thenReportsBothCategoriesAsEmpty() {
+ unit.report(executionContext(), new Violations(List.of(), List.of()));
+
+ snapshotText(log.render());
+ }
+
+ @Test
+ void givenPermissiveViolations_whenReport_thenRendersTable() {
+ unit.report(executionContext(), new Violations(violations(), List.of()));
+
+ snapshotText(log.render());
+ }
+
+ @Test
+ void givenNonPermissiveViolations_whenReport_thenRendersTable() {
+ unit.report(executionContext(), new Violations(List.of(), violations()));
+
+ snapshotText(log.render());
+ }
+
+ /**
+ * Every row of the table, the last one included, has to be padded. The padding of an
+ * {@link de.vandermeer.asciitable.AsciiTable} is applied to the rows added so far, which makes it easy to
+ * leave the row added last unpadded.
+ */
+ @Test
+ void givenViolations_whenReport_thenEveryRowIsPadded() {
+ unit.report(executionContext(), new Violations(violations(), List.of()));
+
+ final List rows = tableRowsOf(log.render());
+
+ Assertions.assertFalse(rows.isEmpty(), "Expected the rendered table to hold rows");
+
+ rows.forEach(row -> cellsOf(row).forEach(cell -> {
+ Assertions.assertTrue(cell.startsWith(" "), () -> "Cell isn't padded to the left: '" + cell + "' of row '" + row + "'");
+ Assertions.assertTrue(cell.endsWith(" "), () -> "Cell isn't padded to the right: '" + cell + "' of row '" + row + "'");
+ }));
+ }
+
+ /**
+ * The violations themselves, and not only the header of the category, have to be reported as a warning.
+ * Reported as info they are dropped by a quiet build, leaving behind a warning without the violations it refers to.
+ */
+ @Test
+ void givenNonPermissiveViolations_whenReport_thenTableIsReportedAsWarning() {
+ unit.report(executionContext(), new Violations(List.of(), violations()));
+
+ assertTableReportedAtLevel("[WARN]");
+ }
+
+ @Test
+ void givenPermissiveViolations_whenReport_thenTableIsReportedAsInfo() {
+ unit.report(executionContext(), new Violations(violations(), List.of()));
+
+ assertTableReportedAtLevel("[INFO]");
+ }
+
+ private void assertTableReportedAtLevel(final String level) {
+ final List tableEntries = log.entries()
+ .stream()
+ .filter(it -> it.contains(TABLE_TOP_LEFT_CORNER))
+ .toList();
+
+ Assertions.assertEquals(1, tableEntries.size(), "Expected the table to be reported exactly once");
+
+ tableEntries.forEach(it -> Assertions.assertTrue(it.startsWith(level), () -> "Expected the table to be reported at " + level));
+ }
+
+ private ExecutionContext executionContext() {
+ return ExecutionContext.of(new MavenProject(), log);
+ }
+
+ /**
+ * Extracts the rows, the horizontal rules excluded, of the rendered table.
+ */
+ private static List tableRowsOf(final String rendered) {
+ return rendered.lines()
+ .filter(line -> line.startsWith(COLUMN_DELIMITER))
+ .toList();
+ }
+
+ /**
+ * Extracts the cells, the leading and trailing delimiter excluded, of a rendered row.
+ */
+ private static List cellsOf(final String row) {
+ final String[] parts = row.split(COLUMN_DELIMITER, -1);
+
+ return Arrays.asList(parts)
+ .subList(1, parts.length - 1);
+ }
+
+ private static List violations() {
+ return List.of(
+ violation("checkstyle", "UnusedImports", "Unused import - java.util.Optional.", "src/main/java/Alpha.java", 3, 8),
+ violation("checkstyle", "LineLength", "Line is longer than 160 characters.", "src/main/java/Beta.java", 42, 1),
+ violation("NullAway", "NullAway", "Passing @Nullable parameter 'name' where @NonNull is required.", "src/main/java/Gamma.java", 17, 24));
+ }
+
+ private static Violation violation(final String tool, final String rule, final String description, final String path,
+ final int line, final int columnNumber) {
+ return Violation.builder()
+ .tool(tool)
+ .rule(rule)
+ .description(description)
+ .fingerprint(tool + ":" + rule + ":" + path)
+ .severity(Severity.MAJOR)
+ .relativePath(path)
+ .fullPath("/workspace/" + path)
+ .line(line)
+ .columnNumber(columnNumber)
+ .build();
+ }
+}
diff --git a/src/test/java/io/github/finoid/maven/plugins/codequality/report/__snapshots__/ConsolePlainViolationReporterUnitTest.snap b/src/test/java/io/github/finoid/maven/plugins/codequality/report/__snapshots__/ConsolePlainViolationReporterUnitTest.snap
new file mode 100644
index 0000000..20bf5e3
--- /dev/null
+++ b/src/test/java/io/github/finoid/maven/plugins/codequality/report/__snapshots__/ConsolePlainViolationReporterUnitTest.snap
@@ -0,0 +1,38 @@
+io.github.finoid.maven.plugins.codequality.report.ConsolePlainViolationReporterUnitTest.givenNoViolations_whenReport_thenReportsBothCategoriesAsEmpty=[
+[INFO] ✅ \u001B[32m ##### No permissive violations found ##### \u001B[0m ✅
+[INFO] ✅ \u001B[32m ##### No non permissive violations found ##### \u001B[0m ✅
+]
+
+
+io.github.finoid.maven.plugins.codequality.report.ConsolePlainViolationReporterUnitTest.givenNonPermissiveViolations_whenReport_thenReportsEveryViolation=[
+[INFO] ✅ \u001B[32m ##### No permissive violations found ##### \u001B[0m ✅
+[WARN] ⚠ \u001B[33m ##### found 2 non permissive violations ##### \u001B[0m
+[WARN] file:///workspace/src/main/java/Alpha.java:3:8
+ [checkstyle - UnusedImports] Unused import - java.util.Optional.
+[WARN] file:///workspace/src/main/java/Gamma.java:17:24
+ [NullAway - NullAway] Passing @Nullable parameter 'name' where @NonNull is required.
+]
+
+
+io.github.finoid.maven.plugins.codequality.report.ConsolePlainViolationReporterUnitTest.givenPermissiveViolations_whenReport_thenReportsEveryViolation=[
+[INFO] ✅ \u001B[32m ##### found 2 permissive violations ##### \u001B[0m
+[INFO] file:///workspace/src/main/java/Alpha.java:3:8
+ [checkstyle - UnusedImports] Unused import - java.util.Optional.
+[INFO] file:///workspace/src/main/java/Gamma.java:17:24
+ [NullAway - NullAway] Passing @Nullable parameter 'name' where @NonNull is required.
+[INFO] ✅ \u001B[32m ##### No non permissive violations found ##### \u001B[0m ✅
+]
+
+
+io.github.finoid.maven.plugins.codequality.report.ConsolePlainViolationReporterUnitTest.givenViolationsOfBothCategories_whenReport_thenReportsEachCategoryAtItsOwnLevel=[
+[INFO] ✅ \u001B[32m ##### found 2 permissive violations ##### \u001B[0m
+[INFO] file:///workspace/src/main/java/Alpha.java:3:8
+ [checkstyle - UnusedImports] Unused import - java.util.Optional.
+[INFO] file:///workspace/src/main/java/Gamma.java:17:24
+ [NullAway - NullAway] Passing @Nullable parameter 'name' where @NonNull is required.
+[WARN] ⚠ \u001B[33m ##### found 2 non permissive violations ##### \u001B[0m
+[WARN] file:///workspace/src/main/java/Alpha.java:3:8
+ [checkstyle - UnusedImports] Unused import - java.util.Optional.
+[WARN] file:///workspace/src/main/java/Gamma.java:17:24
+ [NullAway - NullAway] Passing @Nullable parameter 'name' where @NonNull is required.
+]
\ No newline at end of file
diff --git a/src/test/java/io/github/finoid/maven/plugins/codequality/report/__snapshots__/ConsoleTableViolationReporterUnitTest.snap b/src/test/java/io/github/finoid/maven/plugins/codequality/report/__snapshots__/ConsoleTableViolationReporterUnitTest.snap
new file mode 100644
index 0000000..336acf8
--- /dev/null
+++ b/src/test/java/io/github/finoid/maven/plugins/codequality/report/__snapshots__/ConsoleTableViolationReporterUnitTest.snap
@@ -0,0 +1,54 @@
+io.github.finoid.maven.plugins.codequality.report.ConsoleTableViolationReporterUnitTest.givenNoViolations_whenReport_thenReportsBothCategoriesAsEmpty=[
+[INFO] ✅ \u001B[32m ##### No permissive violations found ##### \u001B[0m ✅
+[INFO] ✅ \u001B[32m ##### No non permissive violations found ##### \u001B[0m ✅
+]
+
+
+io.github.finoid.maven.plugins.codequality.report.ConsoleTableViolationReporterUnitTest.givenNonPermissiveViolations_whenReport_thenRendersTable=[
+[INFO] ✅ \u001B[32m ##### No permissive violations found ##### \u001B[0m ✅
+[WARN] ⚠ \u001B[33m ##### found 3 non permissive violations ##### \u001B[0m
+[WARN]
+┌────────────┬────────────────────┬────────────────────────────────────────────────────────────┬──────────────────────────┬────────────────────┐
+│ │ │ │ │ │
+│ Tool │ Rule │ Description │ Path │ Line/Column number │
+│ │ │ │ │ │
+├────────────┼────────────────────┼────────────────────────────────────────────────────────────┼──────────────────────────┼────────────────────┤
+│ │ │ │ │ │
+│ checkstyle │ UnusedImports │ Unused import - java.util.Optional. │ src/main/java/Alpha.java │ 3:8 │
+│ │ │ │ │ │
+├────────────┼────────────────────┼────────────────────────────────────────────────────────────┼──────────────────────────┼────────────────────┤
+│ │ │ │ │ │
+│ checkstyle │ LineLength │ Line is longer than 160 characters. │ src/main/java/Beta.java │ 42:1 │
+│ │ │ │ │ │
+├────────────┼────────────────────┼────────────────────────────────────────────────────────────┼──────────────────────────┼────────────────────┤
+│ │ │ │ │ │
+│ NullAway │ NullAway │ Passing @Nullable parameter 'name' where @NonNull is │ src/main/java/Gamma.java │ 17:24 │
+│ │ │ required. │ │ │
+│ │ │ │ │ │
+└────────────┴────────────────────┴────────────────────────────────────────────────────────────┴──────────────────────────┴────────────────────┘
+]
+
+
+io.github.finoid.maven.plugins.codequality.report.ConsoleTableViolationReporterUnitTest.givenPermissiveViolations_whenReport_thenRendersTable=[
+[INFO] ✅ \u001B[32m ##### found 3 permissive violations ##### \u001B[0m
+[INFO]
+┌────────────┬────────────────────┬────────────────────────────────────────────────────────────┬──────────────────────────┬────────────────────┐
+│ │ │ │ │ │
+│ Tool │ Rule │ Description │ Path │ Line/Column number │
+│ │ │ │ │ │
+├────────────┼────────────────────┼────────────────────────────────────────────────────────────┼──────────────────────────┼────────────────────┤
+│ │ │ │ │ │
+│ checkstyle │ UnusedImports │ Unused import - java.util.Optional. │ src/main/java/Alpha.java │ 3:8 │
+│ │ │ │ │ │
+├────────────┼────────────────────┼────────────────────────────────────────────────────────────┼──────────────────────────┼────────────────────┤
+│ │ │ │ │ │
+│ checkstyle │ LineLength │ Line is longer than 160 characters. │ src/main/java/Beta.java │ 42:1 │
+│ │ │ │ │ │
+├────────────┼────────────────────┼────────────────────────────────────────────────────────────┼──────────────────────────┼────────────────────┤
+│ │ │ │ │ │
+│ NullAway │ NullAway │ Passing @Nullable parameter 'name' where @NonNull is │ src/main/java/Gamma.java │ 17:24 │
+│ │ │ required. │ │ │
+│ │ │ │ │ │
+└────────────┴────────────────────┴────────────────────────────────────────────────────────────┴──────────────────────────┴────────────────────┘
+[INFO] ✅ \u001B[32m ##### No non permissive violations found ##### \u001B[0m ✅
+]
\ No newline at end of file