From ed79eea481ed073145a82f1bf3520bd55292adf3 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 7 Aug 2026 23:32:21 +0200 Subject: [PATCH] mvnlog: build log viewer, integration tests, script routing Add the mvnlog tool for viewing and analyzing build-report JSON files. Includes BuildReportRenderer for human-readable output, SimpleJsonReader for dependency-free JSON parsing, shell scripts (mvnlog/mvnlog.cmd), and --log routing in mvn/mvn.cmd. Also adds integration tests for build report generation, console modes, and the mvnlog viewer, plus --console=verbose flags for ITs that depend on verbose output. Part 5 of the #12572 split (depends on warning mode PR #12698). Co-Authored-By: Claude Opus 4.6 --- apache-maven/src/assembly/component.xml | 1 + apache-maven/src/assembly/maven/bin/mvn | 18 + apache-maven/src/assembly/maven/bin/mvn.cmd | 2 + apache-maven/src/assembly/maven/bin/mvnlog | 30 + .../src/assembly/maven/bin/mvnlog.cmd | 39 ++ .../apache/maven/api/cli/ParserRequest.java | 24 + .../java/org/apache/maven/api/cli/Tools.java | 3 + .../maven/api/cli/mvnlog/LogOptions.java | 77 +++ .../org/apache/maven/cling/MavenLogCling.java | 95 +++ .../cling/invoker/CommonsCliOptions.java | 5 + .../invoker/mvnlog/BuildReportRenderer.java | 565 ++++++++++++++++++ .../invoker/mvnlog/CommonsCliLogOptions.java | 147 +++++ .../cling/invoker/mvnlog/LogContext.java | 38 ++ .../cling/invoker/mvnlog/LogInvoker.java | 169 ++++++ .../maven/cling/invoker/mvnlog/LogParser.java | 37 ++ .../invoker/mvnlog/SimpleJsonReader.java | 276 +++++++++ .../BuiltinShellCommandRegistryFactory.java | 26 + .../mvnlog/BuildReportRendererTest.java | 250 ++++++++ .../invoker/mvnlog/SimpleJsonReaderTest.java | 141 +++++ its/core-it-suite/pom.xml | 6 +- .../it/MavenITgh12571BuildReportTest.java | 423 +++++++++++++ .../it/MavenITmng5760ResumeFeatureTest.java | 11 + ...ng5965ParallelBuildMultipliesWorkTest.java | 1 + .../MavenITmng6057CheckReactorOrderTest.java | 1 + .../it/MavenITmng6065FailOnSeverityTest.java | 2 + .../it/MavenITmng6118SubmoduleInvocation.java | 6 + .../it/MavenITmng6391PrintVersionTest.java | 2 + ...AnnotationShouldNotReExecuteGoalsTest.java | 3 + ...1ProjectListShouldIncludeChildrenTest.java | 2 + .../MavenITmng7353CliGoalInvocationTest.java | 1 + ...avenITmng7804PluginExecutionOrderTest.java | 1 + .../maven/it/MavenITmng8594AtFileTest.java | 1 + .../resources/gh-12571-build-report/pom.xml | 28 + .../gh-12571-multi-module/module-a/pom.xml | 31 + .../gh-12571-multi-module/module-b/pom.xml | 31 + .../resources/gh-12571-multi-module/pom.xml | 33 + 36 files changed, 2523 insertions(+), 3 deletions(-) create mode 100755 apache-maven/src/assembly/maven/bin/mvnlog create mode 100644 apache-maven/src/assembly/maven/bin/mvnlog.cmd create mode 100644 api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnlog/LogOptions.java create mode 100644 impl/maven-cli/src/main/java/org/apache/maven/cling/MavenLogCling.java create mode 100644 impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/BuildReportRenderer.java create mode 100644 impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/CommonsCliLogOptions.java create mode 100644 impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/LogContext.java create mode 100644 impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/LogInvoker.java create mode 100644 impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/LogParser.java create mode 100644 impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/SimpleJsonReader.java create mode 100644 impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnlog/BuildReportRendererTest.java create mode 100644 impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnlog/SimpleJsonReaderTest.java create mode 100644 its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh12571BuildReportTest.java create mode 100644 its/core-it-suite/src/test/resources/gh-12571-build-report/pom.xml create mode 100644 its/core-it-suite/src/test/resources/gh-12571-multi-module/module-a/pom.xml create mode 100644 its/core-it-suite/src/test/resources/gh-12571-multi-module/module-b/pom.xml create mode 100644 its/core-it-suite/src/test/resources/gh-12571-multi-module/pom.xml diff --git a/apache-maven/src/assembly/component.xml b/apache-maven/src/assembly/component.xml index 5f55a310c8bd..ce303ea4960d 100644 --- a/apache-maven/src/assembly/component.xml +++ b/apache-maven/src/assembly/component.xml @@ -87,6 +87,7 @@ under the License. mvn mvnenc + mvnlog mvnsh mvnup mvnDebug diff --git a/apache-maven/src/assembly/maven/bin/mvn b/apache-maven/src/assembly/maven/bin/mvn index 0adc4eabeb2b..93127f8d8e9f 100755 --- a/apache-maven/src/assembly/maven/bin/mvn +++ b/apache-maven/src/assembly/maven/bin/mvn @@ -303,6 +303,9 @@ handle_args() { --up) MAVEN_MAIN_CLASS="org.apache.maven.cling.MavenUpCling" ;; + --log) + MAVEN_MAIN_CLASS="org.apache.maven.cling.MavenLogCling" + ;; *) ;; esac @@ -311,6 +314,21 @@ handle_args() { } handle_args "$@" + +# Strip routing flags (--debug, --yjp, --enc, --shell, --up, --log) from $@ +# so they are not passed to the Java process where they may collide with +# Commons CLI option-prefix matching (e.g. --log matches --log-file). +_argc=$# +_i=0 +while [ $_i -lt $_argc ]; do + _arg="$1" + shift + case $_arg in + --debug|--yjp|--enc|--shell|--up|--log) ;; + *) set -- "$@" "$_arg" ;; + esac + _i=$((_i + 1)) +done MAVEN_MAIN_CLASS=${MAVEN_MAIN_CLASS:=org.apache.maven.cling.MavenCling} # Build base command string for eval (only contains Maven-controlled values) diff --git a/apache-maven/src/assembly/maven/bin/mvn.cmd b/apache-maven/src/assembly/maven/bin/mvn.cmd index 74d4a5a984d2..7e3c8bdae83b 100644 --- a/apache-maven/src/assembly/maven/bin/mvn.cmd +++ b/apache-maven/src/assembly/maven/bin/mvn.cmd @@ -275,6 +275,8 @@ if "%~1"=="--debug" ( set "MAVEN_MAIN_CLASS=org.apache.maven.cling.MavenShellCling" ) else if "%~1"=="--up" ( set "MAVEN_MAIN_CLASS=org.apache.maven.cling.MavenUpCling" +) else if "%~1"=="--log" ( + set "MAVEN_MAIN_CLASS=org.apache.maven.cling.MavenLogCling" ) exit /b 0 diff --git a/apache-maven/src/assembly/maven/bin/mvnlog b/apache-maven/src/assembly/maven/bin/mvnlog new file mode 100755 index 000000000000..8170bdb16785 --- /dev/null +++ b/apache-maven/src/assembly/maven/bin/mvnlog @@ -0,0 +1,30 @@ +#!/bin/sh + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +# ----------------------------------------------------------------------------- +# Apache Maven Build Log Viewer Script +# +# Environment Variable Prerequisites +# +# JAVA_HOME (Optional) Points to a Java installation. +# MAVEN_OPTS (Optional) Java runtime options used when Maven is executed. +# MAVEN_SKIP_RC (Optional) Flag to disable loading of mavenrc files. +# ----------------------------------------------------------------------------- + +"`dirname "$0"`/mvn" --log "$@" diff --git a/apache-maven/src/assembly/maven/bin/mvnlog.cmd b/apache-maven/src/assembly/maven/bin/mvnlog.cmd new file mode 100644 index 000000000000..7069255cd817 --- /dev/null +++ b/apache-maven/src/assembly/maven/bin/mvnlog.cmd @@ -0,0 +1,39 @@ +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. + +@REM ----------------------------------------------------------------------------- +@REM Apache Maven Build Log Viewer Script +@REM +@REM Environment Variable Prerequisites +@REM +@REM JAVA_HOME (Optional) Points to a Java installation. +@REM MAVEN_BATCH_ECHO (Optional) Set to 'on' to enable the echoing of the batch commands. +@REM MAVEN_BATCH_PAUSE (Optional) set to 'on' to wait for a key stroke before ending. +@REM MAVEN_OPTS (Optional) Java runtime options used when Maven is executed. +@REM MAVEN_SKIP_RC (Optional) Flag to disable loading of mavenrc files. +@REM ----------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%"=="on" echo %MAVEN_BATCH_ECHO% + +@setlocal + +@call "%~dp0"mvn.cmd --log %* diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ParserRequest.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ParserRequest.java index ee25ec63dab3..848151bc2511 100644 --- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ParserRequest.java +++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ParserRequest.java @@ -253,6 +253,30 @@ static Builder mvnup(@Nonnull List args, @Nonnull MessageBuilderFactory return builder(Tools.MVNUP_CMD, Tools.MVNUP_NAME, args, messageBuilderFactory); } + /** + * Creates a new Builder instance for constructing a Maven Build Log Viewer ParserRequest. + * + * @param args the command-line arguments + * @param messageBuilderFactory the factory for creating message builders + * @return a new Builder instance + */ + @Nonnull + static Builder mvnlog(@Nonnull String[] args, @Nonnull MessageBuilderFactory messageBuilderFactory) { + return mvnlog(Arrays.asList(args), messageBuilderFactory); + } + + /** + * Creates a new Builder instance for constructing a Maven Build Log Viewer ParserRequest. + * + * @param args the command-line arguments + * @param messageBuilderFactory the factory for creating message builders + * @return a new Builder instance + */ + @Nonnull + static Builder mvnlog(@Nonnull List args, @Nonnull MessageBuilderFactory messageBuilderFactory) { + return builder(Tools.MVNLOG_CMD, Tools.MVNLOG_NAME, args, messageBuilderFactory); + } + /** * Creates a new Builder instance for constructing a ParserRequest. * diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java index 7559d7ffee06..136268657a91 100644 --- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java +++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Tools.java @@ -42,4 +42,7 @@ private Tools() {} public static final String MVNUP_CMD = "mvnup"; public static final String MVNUP_NAME = "Maven Upgrade Tool"; + + public static final String MVNLOG_CMD = "mvnlog"; + public static final String MVNLOG_NAME = "Maven Build Log Viewer"; } diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnlog/LogOptions.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnlog/LogOptions.java new file mode 100644 index 000000000000..f5cd4de8cb30 --- /dev/null +++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnlog/LogOptions.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.api.cli.mvnlog; + +import java.util.Optional; + +import org.apache.maven.api.annotations.Experimental; +import org.apache.maven.api.cli.Options; + +/** + * Defines the options specific to the Maven build log viewer tool ({@code mvnlog}). + * This interface extends the general {@link Options} interface, adding log-viewing options. + * + * @since 4.1.0 + */ +@Experimental +public interface LogOptions extends Options { + /** + * Whether to show detailed diagnostics (warnings and errors) from the build. + * + * @return an {@link Optional} containing {@code true} if diagnostics should be shown + */ + Optional diagnostics(); + + /** + * Whether to show detailed failure information including stack traces. + * + * @return an {@link Optional} containing {@code true} if failures should be shown in detail + */ + Optional failures(); + + /** + * Whether to show a full per-mojo timing breakdown. + * + * @return an {@link Optional} containing {@code true} if the full breakdown should be shown + */ + Optional full(); + + /** + * Whether to list all available build reports instead of showing one. + * + * @return an {@link Optional} containing {@code true} if reports should be listed + */ + Optional list(); + + /** + * Whether to output the raw JSON build report instead of formatted text. + * Useful for piping to tools like {@code jq} or for programmatic consumption. + * + * @return an {@link Optional} containing {@code true} if raw JSON should be output + */ + Optional json(); + + /** + * Returns the path to a specific build report file to display. + * If not specified, defaults to {@code target/build-reports/build-report-latest.json}. + * + * @return an {@link Optional} containing the report file path, or empty if not specified + */ + Optional reportFile(); +} diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenLogCling.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenLogCling.java new file mode 100644 index 000000000000..baeda012571c --- /dev/null +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/MavenLogCling.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.cling; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.cli.Invoker; +import org.apache.maven.api.cli.Parser; +import org.apache.maven.api.cli.ParserRequest; +import org.apache.maven.cling.invoker.ProtoLookup; +import org.apache.maven.cling.invoker.mvnlog.LogInvoker; +import org.apache.maven.cling.invoker.mvnlog.LogParser; +import org.codehaus.plexus.classworlds.ClassWorld; + +/** + * Maven build log viewer CLI ("new-gen"). + *

+ * Displays formatted summaries of previous Maven build reports. + * Invoked via {@code mvnlog} or {@code mvn --log}. + * + * @since 4.1.0 + */ +public class MavenLogCling extends ClingSupport { + /** + * "Normal" Java entry point. Note: Maven uses ClassWorld Launcher and this entry point is NOT used under normal + * circumstances. + */ + public static void main(String[] args) throws IOException { + int exitCode = new MavenLogCling().run(args, null, null, null, false); + System.exit(exitCode); + } + + /** + * ClassWorld Launcher "enhanced" entry point: returning exitCode and accepts Class World. + */ + public static int main(String[] args, ClassWorld world) throws IOException { + return new MavenLogCling(world).run(args, null, null, null, false); + } + + /** + * ClassWorld Launcher "embedded" entry point: returning exitCode and accepts Class World and streams. + */ + public static int main( + String[] args, + ClassWorld world, + @Nullable InputStream stdIn, + @Nullable OutputStream stdOut, + @Nullable OutputStream stdErr) + throws IOException { + return new MavenLogCling(world).run(args, stdIn, stdOut, stdErr, true); + } + + public MavenLogCling() { + super(); + } + + public MavenLogCling(ClassWorld classWorld) { + super(classWorld); + } + + @Override + protected Invoker createInvoker() { + return new LogInvoker( + ProtoLookup.builder().addMapping(ClassWorld.class, classWorld).build(), null); + } + + @Override + protected Parser createParser() { + return new LogParser(); + } + + @Override + protected ParserRequest.Builder createParserRequestBuilder(String[] args) { + return ParserRequest.mvnlog(args, createMessageBuilderFactory()); + } +} diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CommonsCliOptions.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CommonsCliOptions.java index 77d5faac61be..d75f07b81aec 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CommonsCliOptions.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CommonsCliOptions.java @@ -349,6 +349,7 @@ protected static class CLIManager { public static final String UPGRADE = "up"; public static final String SHELL = "shell"; public static final String YJP = "yjp"; + public static final String LOG = "log"; // deprecated ones @Deprecated @@ -498,6 +499,10 @@ protected void prepareOptions(org.apache.commons.cli.Options options) { .longOpt(YJP) .desc("Launch the JVM with Yourkit profiler (script option).") .get()); + options.addOption(Option.builder() + .longOpt(LOG) + .desc("Launch the Maven Build Log Viewer (script option).") + .get()); // Deprecated options.addOption(Option.builder(ALTERNATE_GLOBAL_SETTINGS) diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/BuildReportRenderer.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/BuildReportRenderer.java new file mode 100644 index 000000000000..3448c8c00fab --- /dev/null +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/BuildReportRenderer.java @@ -0,0 +1,565 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.cling.invoker.mvnlog; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.apache.maven.api.services.MessageBuilder; +import org.apache.maven.api.services.MessageBuilderFactory; + +/** + * Renders a build report (parsed from JSON into a {@code Map}) + * as formatted terminal output. + *

+ * Shared between the standalone {@code mvnlog} command and the {@code mvnsh} subcommand. + * + * @since 4.1.0 + */ +public class BuildReportRenderer { + + private static final int MAX_PADDED_BUILD_TIME_DURATION_LENGTH = 9; + + private final MessageBuilderFactory messageBuilderFactory; + private final Consumer output; + + public BuildReportRenderer(MessageBuilderFactory messageBuilderFactory, Consumer output) { + this.messageBuilderFactory = messageBuilderFactory; + this.output = output; + } + + /** + * Render the default summary view of a build report. + */ + @SuppressWarnings("unchecked") + public void renderSummary(Map report) { + renderHeader(report); + + // Module summary + List> modules = getList(report, "modules"); + if (!modules.isEmpty()) { + for (Map module : modules) { + renderModuleLine(module); + } + output.accept(""); + + // Stats line + int passed = 0, failed = 0, skipped = 0; + for (Map module : modules) { + String status = getString(module, "status"); + switch (status) { + case "SUCCESS": + passed++; + break; + case "FAILURE": + failed++; + break; + default: + skipped++; + break; + } + } + StringBuilder stats = new StringBuilder(); + stats.append(modules.size()).append(" modules"); + stats.append(" | ").append(passed).append(" passed"); + if (failed > 0) { + stats.append(" | ").append(failed).append(" failed"); + } + if (skipped > 0) { + stats.append(" | ").append(skipped).append(" skipped"); + } + output.accept(stats.toString()); + } + + // Problems — show structured warnings/errors so the user doesn't have to re-run the build + List> problems = getList(report, "problems"); + if (!problems.isEmpty()) { + long warnings = problems.stream() + .filter(d -> "WARNING".equals(getString(d, "severity"))) + .count(); + long errors = problems.stream() + .filter(d -> "ERROR".equals(getString(d, "severity"))) + .count(); + if (warnings > 0 || errors > 0) { + output.accept(""); + MessageBuilder header = messageBuilderFactory.builder(); + header.a("Problems: "); + if (errors > 0) { + header.failure(errors + " error" + (errors > 1 ? "s" : "")); + } + if (errors > 0 && warnings > 0) { + header.a(", "); + } + if (warnings > 0) { + header.warning(warnings + " warning" + (warnings > 1 ? "s" : "")); + } + output.accept(header.toString()); + + // Show each problem with structured details + for (Map p : problems) { + renderProblemCompact(p); + } + } + } + + // Failures count + List> failures = getList(report, "failures"); + if (!failures.isEmpty()) { + output.accept(messageBuilderFactory + .builder() + .failure(failures.size() + " failure" + (failures.size() > 1 ? "s" : "")) + .toString()); + } + + output.accept(""); + String duration = getString(report, "duration"); + output.accept("Total time: " + (duration != null ? formatDuration(duration) : "?")); + } + + /** + * Render the detailed diagnostics view. + */ + @SuppressWarnings("unchecked") + public void renderDiagnostics(Map report) { + renderHeader(report); + + List> problems = getList(report, "problems"); + if (problems.isEmpty()) { + output.accept(messageBuilderFactory + .builder() + .success("No problems recorded.") + .toString()); + return; + } + + // Count by severity + long errors = problems.stream() + .filter(p -> "ERROR".equals(getString(p, "severity"))) + .count(); + long warnings = problems.stream() + .filter(p -> "WARNING".equals(getString(p, "severity"))) + .count(); + long infos = problems.size() - errors - warnings; + + MessageBuilder header = messageBuilderFactory.builder(); + header.strong("Problems (" + problems.size() + ")"); + header.a(": "); + List parts = new ArrayList<>(); + if (errors > 0) { + parts.add(errors + " error" + (errors > 1 ? "s" : "")); + } + if (warnings > 0) { + parts.add(warnings + " warning" + (warnings > 1 ? "s" : "")); + } + if (infos > 0) { + parts.add(infos + " info"); + } + header.a(String.join(", ", parts)); + output.accept(header.toString()); + output.accept(""); + + for (Map problem : problems) { + renderProblemDetailed(problem); + } + } + + /** + * Render the detailed failures view. + */ + @SuppressWarnings("unchecked") + public void renderFailures(Map report) { + renderHeader(report); + + List> failures = getList(report, "failures"); + if (failures.isEmpty()) { + output.accept(messageBuilderFactory + .builder() + .success("No failures recorded.") + .toString()); + return; + } + + output.accept("Failures (" + failures.size() + "):"); + for (Map failure : failures) { + output.accept(""); + String module = getString(failure, "module"); + String mojo = getString(failure, "mojo"); + MessageBuilder mb = messageBuilderFactory.builder(); + mb.failure(" [FAIL] ").a(module); + if (mojo != null && !mojo.isEmpty()) { + mb.a(" - ").a(mojo); + } + output.accept(mb.toString()); + + String message = getString(failure, "message"); + if (message != null) { + output.accept(" " + message); + } + + String stackTrace = getString(failure, "stackTrace"); + if (stackTrace != null && !stackTrace.isEmpty()) { + // Show first few lines of stack trace + String[] lines = stackTrace.split("\n"); + int limit = Math.min(lines.length, 10); + for (int i = 0; i < limit; i++) { + output.accept(" " + lines[i]); + } + if (lines.length > limit) { + output.accept(" ... " + (lines.length - limit) + " more lines"); + } + } + } + } + + /** + * Render the full per-mojo timing breakdown. + */ + @SuppressWarnings("unchecked") + public void renderFull(Map report) { + renderHeader(report); + + List> modules = getList(report, "modules"); + if (modules.isEmpty()) { + output.accept("No modules recorded."); + return; + } + + for (Map module : modules) { + String artifactId = getString(module, "artifactId"); + String status = getString(module, "status"); + String duration = getString(module, "duration"); + + MessageBuilder mb = messageBuilderFactory.builder(); + mb.strong("Module: " + artifactId); + mb.a(" (").a(duration != null ? formatDuration(duration) : "?").a(") "); + if ("SUCCESS".equals(status)) { + mb.success(status); + } else if ("FAILURE".equals(status)) { + mb.failure(status); + } else { + mb.warning(status); + } + output.accept(mb.toString()); + + List> mojos = getList(module, "mojos"); + for (Map mojo : mojos) { + String goal = getString(mojo, "goal"); + String mojoArtifactId = getString(mojo, "artifactId"); + String mojoDuration = getString(mojo, "duration"); + String mojoStatus = getString(mojo, "status"); + String executionId = getString(mojo, "executionId"); + + StringBuilder line = new StringBuilder(" "); + String prefix = mojoArtifactId != null + ? mojoArtifactId.replace("maven-", "").replace("-plugin", "") + : ""; + line.append(prefix); + if (goal != null) { + line.append(":").append(goal); + } + if (executionId != null && !executionId.isEmpty()) { + line.append(" (").append(executionId).append(")"); + } + + // Pad with dots + int padTo = 50; + while (line.length() < padTo) { + line.append('.'); + } + line.append(' '); + + MessageBuilder mojoMb = messageBuilderFactory.builder(); + mojoMb.a(line); + mojoMb.a(mojoDuration != null ? formatDuration(mojoDuration) : "?"); + if ("FAILURE".equals(mojoStatus)) { + mojoMb.a(" ").failure("FAILED"); + } + output.accept(mojoMb.toString()); + } + output.accept(""); + } + } + + /** + * List all available build report files in the given directory. + */ + public void listReports(Path buildReportsDir) throws IOException { + if (!Files.isDirectory(buildReportsDir)) { + output.accept("No build reports directory found at: " + buildReportsDir); + return; + } + + List reports = new ArrayList<>(); + try (DirectoryStream stream = Files.newDirectoryStream(buildReportsDir, "build-report-*.json")) { + for (Path entry : stream) { + if (Files.isRegularFile(entry) + && !entry.getFileName().toString().equals("build-report-latest.json")) { + reports.add(entry); + } + } + } + + if (reports.isEmpty()) { + output.accept("No build reports found in: " + buildReportsDir); + return; + } + + reports.sort(Comparator.comparing(Path::getFileName).reversed()); + + output.accept(messageBuilderFactory + .builder() + .strong("Available build reports:") + .toString()); + output.accept(""); + + Path latestLink = buildReportsDir.resolve("build-report-latest.json"); + Path latestTarget = null; + if (Files.isSymbolicLink(latestLink)) { + try { + latestTarget = Files.readSymbolicLink(latestLink).getFileName(); + } catch (IOException e) { + // ignore + } + } + + for (Path report : reports) { + String name = report.getFileName().toString(); + StringBuilder line = new StringBuilder(" "); + line.append(name); + if (latestTarget != null && name.equals(latestTarget.toString())) { + line.append(" <- latest"); + } + output.accept(line.toString()); + } + } + + // ---- Problem rendering ---- + + /** + * Compact problem rendering for the default summary view. + * Shows severity, message, source, and suggestion on two lines. + */ + private void renderProblemCompact(Map problem) { + String severity = getString(problem, "severity"); + String message = getString(problem, "message"); + String source = getString(problem, "source"); + String suggestion = getString(problem, "suggestion"); + + MessageBuilder mb = messageBuilderFactory.builder(); + if ("ERROR".equals(severity)) { + mb.failure(" [ERROR] "); + } else if ("WARNING".equals(severity)) { + mb.warning(" [WARN] "); + } else { + mb.a(" [INFO] "); + } + mb.a(message); + if (source != null && !source.isEmpty()) { + mb.a(" ").a(messageBuilderFactory.builder().strong(source).toString()); + } + output.accept(mb.toString()); + + if (suggestion != null && !suggestion.isEmpty()) { + output.accept(" suggestion: " + suggestion); + } + } + + /** + * Detailed problem rendering for the {@code --diagnostics} view. + * Shows all available fields: key, severity, message, source, location, + * suggestion, and documentation URL. + */ + private void renderProblemDetailed(Map problem) { + String severity = getString(problem, "severity"); + String message = getString(problem, "message"); + String key = getString(problem, "key"); + String source = getString(problem, "source"); + String suggestion = getString(problem, "suggestion"); + String docUrl = getString(problem, "documentationUrl"); + + // Severity label + message + MessageBuilder mb = messageBuilderFactory.builder(); + if ("ERROR".equals(severity)) { + mb.failure(" [ERROR] "); + } else if ("WARNING".equals(severity)) { + mb.warning(" [WARN] "); + } else { + mb.a(" [INFO] "); + } + mb.a(message); + output.accept(mb.toString()); + + // Key (diagnostic identifier for suppression) + if (key != null && !key.isEmpty()) { + output.accept(" key: " + key); + } + + // Source + location + if (source != null && !source.isEmpty()) { + StringBuilder loc = new StringBuilder(" source: "); + loc.append(source); + Number line = getNumber(problem, "line"); + if (line != null && line.intValue() > 0) { + loc.append(":").append(line.intValue()); + Number column = getNumber(problem, "column"); + if (column != null && column.intValue() > 0) { + loc.append(":").append(column.intValue()); + } + } + output.accept(loc.toString()); + } + + // Suggestion + if (suggestion != null && !suggestion.isEmpty()) { + MessageBuilder sugMb = messageBuilderFactory.builder(); + sugMb.a(" suggestion: ").success(suggestion); + output.accept(sugMb.toString()); + } + + // Documentation URL + if (docUrl != null && !docUrl.isEmpty()) { + output.accept(" docs: " + docUrl); + } + + output.accept(""); + } + + // ---- Internal helpers ---- + + private void renderHeader(Map report) { + String mavenVersion = getString(report, "mavenVersion"); + String startTime = getString(report, "startTime"); + + MessageBuilder header = messageBuilderFactory.builder(); + header.strong("Build Report"); + if (mavenVersion != null) { + header.a(" — Maven ").a(mavenVersion); + } + if (startTime != null) { + header.a(" — ").a(startTime); + } + output.accept(header.toString()); + + // Result line + String status = getString(report, "status"); + MessageBuilder result = messageBuilderFactory.builder(); + if ("FAILURE".equals(status)) { + result.failure("BUILD FAILURE"); + } else { + result.success("BUILD SUCCESS"); + } + output.accept(result.toString()); + output.accept(""); + } + + private void renderModuleLine(Map module) { + String artifactId = getString(module, "artifactId"); + String status = getString(module, "status"); + String duration = getString(module, "duration"); + + StringBuilder buffer = new StringBuilder(128); + + // Status marker + buffer.append(' '); + + buffer.append(artifactId); + buffer.append(' '); + + // Pad with dots + int maxLen = 60; + if (buffer.length() <= maxLen) { + while (buffer.length() < maxLen) { + buffer.append('.'); + } + buffer.append(' '); + } + + MessageBuilder mb = messageBuilderFactory.builder(); + mb.a(buffer); + switch (status) { + case "SUCCESS": + mb.success(status); + break; + case "FAILURE": + mb.failure(status); + break; + default: + mb.warning(status); + break; + } + + // Duration + if (duration != null) { + mb.a(" [").a(formatDuration(duration)).a("]"); + } + + output.accept(mb.toString()); + } + + /** + * Format an ISO-8601 duration string (e.g. "PT2.1S") into a human-readable form. + */ + static String formatDuration(String isoDuration) { + try { + Duration d = Duration.parse(isoDuration); + long totalSeconds = d.getSeconds(); + int millis = d.getNano() / 1_000_000; + + if (totalSeconds >= 60) { + long minutes = totalSeconds / 60; + long seconds = totalSeconds % 60; + return String.format("%d:%02d min", minutes, seconds); + } else { + return String.format("%d.%03d s", totalSeconds, millis); + } + } catch (Exception e) { + return isoDuration; // fallback to raw string + } + } + + @SuppressWarnings("unchecked") + private static List> getList(Map map, String key) { + Object value = map.get(key); + if (value instanceof List) { + return (List>) value; + } + return List.of(); + } + + private static String getString(Map map, String key) { + Object value = map.get(key); + return value != null ? value.toString() : null; + } + + private static Number getNumber(Map map, String key) { + Object value = map.get(key); + if (value instanceof Number) { + return (Number) value; + } + return null; + } +} diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/CommonsCliLogOptions.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/CommonsCliLogOptions.java new file mode 100644 index 000000000000..5f551644d35a --- /dev/null +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/CommonsCliLogOptions.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.cling.invoker.mvnlog; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.ParseException; +import org.apache.maven.api.cli.Options; +import org.apache.maven.api.cli.ParserRequest; +import org.apache.maven.api.cli.mvnlog.LogOptions; +import org.apache.maven.cling.invoker.CommonsCliOptions; + +/** + * Implementation of {@link LogOptions} using Commons CLI. + */ +public class CommonsCliLogOptions extends CommonsCliOptions implements LogOptions { + + public static CommonsCliLogOptions parse(String[] args) throws ParseException { + CLIManager cliManager = new CLIManager(); + return new CommonsCliLogOptions(Options.SOURCE_CLI, cliManager, cliManager.parse(args)); + } + + protected CommonsCliLogOptions(String source, CLIManager cliManager, CommandLine commandLine) { + super(source, cliManager, commandLine); + } + + @Override + public Optional diagnostics() { + if (commandLine.hasOption(CLIManager.DIAGNOSTICS)) { + return Optional.of(Boolean.TRUE); + } + return Optional.empty(); + } + + @Override + public Optional failures() { + if (commandLine.hasOption(CLIManager.FAILURES)) { + return Optional.of(Boolean.TRUE); + } + return Optional.empty(); + } + + @Override + public Optional full() { + if (commandLine.hasOption(CLIManager.FULL)) { + return Optional.of(Boolean.TRUE); + } + return Optional.empty(); + } + + @Override + public Optional list() { + if (commandLine.hasOption(CLIManager.LIST)) { + return Optional.of(Boolean.TRUE); + } + return Optional.empty(); + } + + @Override + public Optional json() { + if (commandLine.hasOption(CLIManager.JSON)) { + return Optional.of(Boolean.TRUE); + } + return Optional.empty(); + } + + @Override + public Optional reportFile() { + List args = commandLine.getArgList(); + if (!args.isEmpty()) { + return Optional.of(args.get(0)); + } + return Optional.empty(); + } + + @Override + public void displayHelp(ParserRequest request, Consumer printStream) { + super.displayHelp(request, printStream); + printStream.accept(""); + printStream.accept("Usage: mvnlog [options] [report-file]"); + printStream.accept(""); + printStream.accept("Displays a formatted summary of the last Maven build report."); + printStream.accept("If no report-file is specified, reads target/build-reports/build-report-latest.json."); + printStream.accept(""); + printStream.accept("Use --json to output the raw JSON report (e.g. mvnlog --json | jq '.modules')."); + printStream.accept(""); + } + + @Override + protected CommonsCliLogOptions copy( + String source, CommonsCliOptions.CLIManager cliManager, CommandLine commandLine) { + return new CommonsCliLogOptions(source, (CLIManager) cliManager, commandLine); + } + + protected static class CLIManager extends CommonsCliOptions.CLIManager { + public static final String DIAGNOSTICS = "d"; + public static final String FAILURES = "f"; + public static final String FULL = "F"; + public static final String LIST = "L"; + public static final String JSON = "j"; + + @Override + protected void prepareOptions(org.apache.commons.cli.Options options) { + super.prepareOptions(options); + options.addOption(Option.builder(DIAGNOSTICS) + .longOpt("diagnostics") + .desc("Show detailed warnings and errors from the build") + .get()); + options.addOption(Option.builder(FAILURES) + .longOpt("failures") + .desc("Show detailed failure information including stack traces") + .get()); + options.addOption(Option.builder(FULL) + .longOpt("full") + .desc("Show full per-mojo timing breakdown") + .get()); + options.addOption(Option.builder(LIST) + .longOpt("list") + .desc("List all available build reports") + .get()); + options.addOption(Option.builder(JSON) + .longOpt("json") + .desc("Output the raw JSON build report (useful for piping to jq)") + .get()); + } + } +} diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/LogContext.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/LogContext.java new file mode 100644 index 000000000000..cf6193e19a67 --- /dev/null +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/LogContext.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.cling.invoker.mvnlog; + +import org.apache.maven.api.cli.InvokerRequest; +import org.apache.maven.api.cli.mvnlog.LogOptions; +import org.apache.maven.cling.invoker.LookupContext; + +/** + * Context for the {@code mvnlog} build log viewer. + */ +@SuppressWarnings("VisibilityModifier") +public class LogContext extends LookupContext { + public LogContext(InvokerRequest invokerRequest, LogOptions logOptions) { + super(invokerRequest, true, logOptions); + } + + @Override + public LogOptions options() { + return (LogOptions) super.options(); + } +} diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/LogInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/LogInvoker.java new file mode 100644 index 000000000000..e7549944f06f --- /dev/null +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/LogInvoker.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.cling.invoker.mvnlog; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Consumer; + +import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.cli.InvokerRequest; +import org.apache.maven.api.cli.mvnlog.LogOptions; +import org.apache.maven.api.services.Lookup; +import org.apache.maven.cling.invoker.LookupContext; +import org.apache.maven.cling.invoker.LookupInvoker; + +/** + * Invoker for the {@code mvnlog} build log viewer. + *

+ * This is a lightweight invoker that does NOT set up the DI container, + * Maven settings, or any build infrastructure. It only needs a terminal + * (for colors and width detection) and the parsed CLI options. + * + * @since 4.1.0 + */ +public class LogInvoker extends LookupInvoker { + + public static final int OK = 0; + public static final int ERROR = 1; + public static final int BAD_INPUT = 2; + + private static final String DEFAULT_REPORT_DIR = "target/build-reports"; + private static final String DEFAULT_REPORT_FILE = "build-report-latest.json"; + + public LogInvoker(Lookup protoLookup, @Nullable Consumer contextConsumer) { + super(protoLookup, contextConsumer); + } + + @Override + protected LogContext createContext(InvokerRequest invokerRequest) { + return new LogContext( + invokerRequest, (LogOptions) invokerRequest.options().orElse(null)); + } + + /** + * Override doInvoke to skip the heavyweight DI container, settings, + * and repository setup that mvnlog does not need. + */ + @Override + protected int doInvoke(LogContext context) throws Exception { + validate(context); + pushCoreProperties(context); + configureLogging(context); + createTerminal(context); + activateLogging(context); + helpOrVersionAndMayExit(context); + return execute(context); + } + + @Override + protected void lookup(LogContext context) throws Exception { + // No DI container needed for log viewing + } + + @Override + protected int execute(LogContext context) throws Exception { + LogOptions options = context.options(); + Consumer output = line -> { + if (context.writer != null) { + context.writer.accept(line); + } else { + context.logger.info(line); + } + }; + + BuildReportRenderer renderer = new BuildReportRenderer(context.invokerRequest.messageBuilderFactory(), output); + + // Handle --list: show available reports + if (options != null && options.list().orElse(false)) { + Path reportDir = resolveReportDir(context); + renderer.listReports(reportDir); + return OK; + } + + // Resolve and read the report file + Path reportFile = resolveReportFile(context); + if (!Files.isRegularFile(reportFile)) { + context.logger.error("Build report not found: " + reportFile); + context.logger.error("Run a Maven build first, then use mvnlog to view the report."); + return BAD_INPUT; + } + + String json; + try { + json = Files.readString(reportFile); + } catch (IOException e) { + context.logger.error("Failed to read report file: " + e.getMessage()); + return ERROR; + } + + // Handle --json: output the raw JSON and exit + if (options != null && options.json().orElse(false)) { + output.accept(json); + return OK; + } + + Map report; + try { + report = SimpleJsonReader.parse(json); + } catch (IllegalArgumentException e) { + context.logger.error("Failed to parse report file: " + e.getMessage()); + return ERROR; + } + + // Render based on flags + if (options != null && options.full().orElse(false)) { + renderer.renderFull(report); + } else if (options != null && options.failures().orElse(false)) { + renderer.renderFailures(report); + } else if (options != null && options.diagnostics().orElse(false)) { + renderer.renderDiagnostics(report); + } else { + renderer.renderSummary(report); + } + + return OK; + } + + private Path resolveReportDir(LogContext context) { + Path cwd = context.invokerRequest.cwd(); + return cwd.resolve(DEFAULT_REPORT_DIR); + } + + private Path resolveReportFile(LogContext context) { + LogOptions options = context.options(); + + // Explicit report file path from command line + if (options != null) { + String reportFile = options.reportFile().orElse(null); + if (reportFile != null) { + Path path = Path.of(reportFile); + if (path.isAbsolute()) { + return path; + } + return context.invokerRequest.cwd().resolve(path); + } + } + + // Default: target/build-reports/build-report-latest.json + return resolveReportDir(context).resolve(DEFAULT_REPORT_FILE); + } +} diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/LogParser.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/LogParser.java new file mode 100644 index 000000000000..a26219ec8a01 --- /dev/null +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/LogParser.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.cling.invoker.mvnlog; + +import org.apache.commons.cli.ParseException; +import org.apache.maven.api.cli.Options; +import org.apache.maven.cling.invoker.BaseParser; + +/** + * Parser for the {@code mvnlog} command-line arguments. + */ +public class LogParser extends BaseParser { + @Override + protected Options parseCliOptions(LocalContext context) { + try { + return CommonsCliLogOptions.parse(context.parserRequest.args().toArray(new String[0])); + } catch (ParseException e) { + throw new IllegalArgumentException("Failed to parse command line options: " + e.getMessage(), e); + } + } +} diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/SimpleJsonReader.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/SimpleJsonReader.java new file mode 100644 index 000000000000..818ad1cad136 --- /dev/null +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnlog/SimpleJsonReader.java @@ -0,0 +1,276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.cling.invoker.mvnlog; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Minimal recursive-descent JSON parser that reads a JSON string into + * {@code Map} / {@code List} / {@code String} / {@code Number} / {@code Boolean} / null. + *

+ * No external dependencies. Companion to {@code BuildReportJsonWriter} which writes + * JSON without any library; this reader follows the same zero-dependency principle. + *

+ * This parser handles the full JSON spec (objects, arrays, strings with escapes, + * numbers, booleans, null) and is sufficient for reading Maven build report files. + */ +final class SimpleJsonReader { + + private final String json; + private int pos; + + private SimpleJsonReader(String json) { + this.json = json; + this.pos = 0; + } + + /** + * Parse a JSON string into a nested structure of Maps, Lists, and primitives. + * + * @param json the JSON string to parse + * @return the parsed value (typically a {@code Map} for a JSON object) + * @throws IllegalArgumentException if the JSON is malformed + */ + @SuppressWarnings("unchecked") + static Map parse(String json) { + SimpleJsonReader reader = new SimpleJsonReader(json.strip()); + Object result = reader.parseValue(); + if (!(result instanceof Map)) { + throw new IllegalArgumentException("Expected JSON object at root"); + } + return (Map) result; + } + + private Object parseValue() { + skipWhitespace(); + if (pos >= json.length()) { + throw error("Unexpected end of input"); + } + char c = json.charAt(pos); + if (c == '{') { + return parseObject(); + } + if (c == '[') { + return parseArray(); + } + if (c == '"') { + return parseString(); + } + if (c == 't' || c == 'f') { + return parseBoolean(); + } + if (c == 'n') { + return parseNull(); + } + if (c == '-' || (c >= '0' && c <= '9')) { + return parseNumber(); + } + throw error("Unexpected character: " + c); + } + + private Map parseObject() { + expect('{'); + Map map = new LinkedHashMap<>(); + skipWhitespace(); + if (pos < json.length() && json.charAt(pos) == '}') { + pos++; + return map; + } + while (true) { + skipWhitespace(); + String key = parseString(); + skipWhitespace(); + expect(':'); + Object value = parseValue(); + map.put(key, value); + skipWhitespace(); + if (pos < json.length() && json.charAt(pos) == ',') { + pos++; + } else { + break; + } + } + skipWhitespace(); + expect('}'); + return map; + } + + private List parseArray() { + expect('['); + List list = new ArrayList<>(); + skipWhitespace(); + if (pos < json.length() && json.charAt(pos) == ']') { + pos++; + return list; + } + while (true) { + list.add(parseValue()); + skipWhitespace(); + if (pos < json.length() && json.charAt(pos) == ',') { + pos++; + } else { + break; + } + } + skipWhitespace(); + expect(']'); + return list; + } + + private String parseString() { + expect('"'); + StringBuilder sb = new StringBuilder(); + while (pos < json.length()) { + char c = json.charAt(pos++); + if (c == '"') { + return sb.toString(); + } + if (c == '\\') { + if (pos >= json.length()) { + throw error("Unexpected end of string escape"); + } + char escaped = json.charAt(pos++); + switch (escaped) { + case '"': + sb.append('"'); + break; + case '\\': + sb.append('\\'); + break; + case '/': + sb.append('/'); + break; + case 'n': + sb.append('\n'); + break; + case 'r': + sb.append('\r'); + break; + case 't': + sb.append('\t'); + break; + case 'b': + sb.append('\b'); + break; + case 'f': + sb.append('\f'); + break; + case 'u': + if (pos + 4 > json.length()) { + throw error("Incomplete unicode escape"); + } + String hex = json.substring(pos, pos + 4); + sb.append((char) Integer.parseInt(hex, 16)); + pos += 4; + break; + default: + sb.append(escaped); + } + } else { + sb.append(c); + } + } + throw error("Unterminated string"); + } + + private Number parseNumber() { + int start = pos; + if (pos < json.length() && json.charAt(pos) == '-') { + pos++; + } + while (pos < json.length() && json.charAt(pos) >= '0' && json.charAt(pos) <= '9') { + pos++; + } + boolean isFloat = false; + if (pos < json.length() && json.charAt(pos) == '.') { + isFloat = true; + pos++; + while (pos < json.length() && json.charAt(pos) >= '0' && json.charAt(pos) <= '9') { + pos++; + } + } + if (pos < json.length() && (json.charAt(pos) == 'e' || json.charAt(pos) == 'E')) { + isFloat = true; + pos++; + if (pos < json.length() && (json.charAt(pos) == '+' || json.charAt(pos) == '-')) { + pos++; + } + while (pos < json.length() && json.charAt(pos) >= '0' && json.charAt(pos) <= '9') { + pos++; + } + } + String numStr = json.substring(start, pos); + if (isFloat) { + return Double.parseDouble(numStr); + } + long value = Long.parseLong(numStr); + if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) { + return (int) value; + } + return value; + } + + private Boolean parseBoolean() { + if (json.startsWith("true", pos)) { + pos += 4; + return Boolean.TRUE; + } + if (json.startsWith("false", pos)) { + pos += 5; + return Boolean.FALSE; + } + throw error("Expected boolean"); + } + + private Object parseNull() { + if (json.startsWith("null", pos)) { + pos += 4; + return null; + } + throw error("Expected null"); + } + + private void skipWhitespace() { + while (pos < json.length()) { + char c = json.charAt(pos); + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + pos++; + } else { + break; + } + } + } + + private void expect(char expected) { + if (pos >= json.length() || json.charAt(pos) != expected) { + throw error("Expected '" + expected + "'"); + } + pos++; + } + + private IllegalArgumentException error(String message) { + int contextStart = Math.max(0, pos - 20); + int contextEnd = Math.min(json.length(), pos + 20); + String context = json.substring(contextStart, contextEnd); + return new IllegalArgumentException(message + " at position " + pos + " near: ..." + context + "..."); + } +} diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/builtin/BuiltinShellCommandRegistryFactory.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/builtin/BuiltinShellCommandRegistryFactory.java index a221e1b5b152..18ee72f9d0f9 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/builtin/BuiltinShellCommandRegistryFactory.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/builtin/BuiltinShellCommandRegistryFactory.java @@ -43,6 +43,8 @@ import org.apache.maven.cling.invoker.mvnenc.EncryptInvoker; import org.apache.maven.cling.invoker.mvnenc.EncryptParser; import org.apache.maven.cling.invoker.mvnenc.Goal; +import org.apache.maven.cling.invoker.mvnlog.LogInvoker; +import org.apache.maven.cling.invoker.mvnlog.LogParser; import org.apache.maven.cling.invoker.mvnsh.ShellCommandRegistryFactory; import org.apache.maven.cling.invoker.mvnup.UpgradeInvoker; import org.apache.maven.cling.invoker.mvnup.UpgradeParser; @@ -75,6 +77,8 @@ private static class BuiltinShellCommandRegistry extends JlineCommandRegistry im private final EncryptParser encryptParser; private final UpgradeInvoker shellUpgradeInvoker; private final UpgradeParser upgradeParser; + private final LogInvoker shellLogInvoker; + private final LogParser logParser; private BuiltinShellCommandRegistry(LookupContext shellContext) { this.shellContext = requireNonNull(shellContext, "shellContext"); @@ -84,12 +88,15 @@ private BuiltinShellCommandRegistry(LookupContext shellContext) { this.encryptParser = new EncryptParser(); this.shellUpgradeInvoker = new UpgradeInvoker(shellContext.invokerRequest.lookup(), contextCopier()); this.upgradeParser = new UpgradeParser(); + this.shellLogInvoker = new LogInvoker(shellContext.invokerRequest.lookup(), contextCopier()); + this.logParser = new LogParser(); Map commandExecute = new HashMap<>(); commandExecute.put("!", new CommandMethods(this::shell, this::defaultCompleter)); commandExecute.put("cd", new CommandMethods(this::cd, this::cdCompleter)); commandExecute.put("pwd", new CommandMethods(this::pwd, this::defaultCompleter)); commandExecute.put("mvn", new CommandMethods(this::mvn, this::mvnCompleter)); commandExecute.put("mvnenc", new CommandMethods(this::mvnenc, this::mvnencCompleter)); + commandExecute.put("mvnlog", new CommandMethods(this::mvnlog, this::mvnlogCompleter)); commandExecute.put("mvnup", new CommandMethods(this::mvnup, this::mvnupCompleter)); registerCommands(commandExecute); } @@ -121,6 +128,7 @@ private Consumer contextCopier() { public void close() throws Exception { shellMavenInvoker.close(); shellEncryptInvoker.close(); + shellLogInvoker.close(); shellUpgradeInvoker.close(); } @@ -251,6 +259,24 @@ private List mvnencCompleter(String name) { shellContext.lookup.lookupMap(Goal.class).keySet()))); } + private void mvnlog(CommandInput input) { + try { + shellLogInvoker.invoke(logParser.parseInvocation( + ParserRequest.mvnlog(input.args(), shellContext.invokerRequest.messageBuilderFactory()) + .cwd(shellContext.cwd.get()) + .build())); + } catch (InvokerException.ExitException e) { + shellContext.logger.error("mvnlog command exited with exit code " + e.getExitCode()); + } catch (Exception e) { + saveException(e); + } + } + + private List mvnlogCompleter(String name) { + return List.of( + new ArgumentCompleter(new StringsCompleter("--diagnostics", "--failures", "--full", "--list"))); + } + private void mvnup(CommandInput input) { try { shellUpgradeInvoker.invoke(upgradeParser.parseInvocation( diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnlog/BuildReportRendererTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnlog/BuildReportRendererTest.java new file mode 100644 index 000000000000..8e7286718960 --- /dev/null +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnlog/BuildReportRendererTest.java @@ -0,0 +1,250 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.cling.invoker.mvnlog; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.maven.jline.JLineMessageBuilderFactory; +import org.apache.maven.jline.MessageUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BuildReportRendererTest { + + private final JLineMessageBuilderFactory messageBuilderFactory = new JLineMessageBuilderFactory(); + + @BeforeAll + static void setUp() { + MessageUtils.setColorEnabled(false); + } + + @AfterAll + static void tearDown() { + MessageUtils.setColorEnabled(true); + } + + private Map createSuccessReport() { + String json = """ + { + "formatVersion": "1.0", + "status": "SUCCESS", + "duration": "PT6.7S", + "startTime": "2026-07-29T10:00:00Z", + "mavenVersion": "4.1.0-SNAPSHOT", + "javaVersion": "21.0.1", + "goals": ["clean", "install"], + "project": "org.example:root", + "multiModule": true, + "threads": 1, + "modules": [ + { + "groupId": "org.example", + "artifactId": "api", + "version": "1.0", + "status": "SUCCESS", + "startTime": "2026-07-29T10:00:01Z", + "duration": "PT2.1S", + "mojos": [ + { + "groupId": "org.apache.maven.plugins", + "artifactId": "maven-compiler-plugin", + "version": "3.15.0", + "goal": "compile", + "executionId": "default-compile", + "phase": "compile", + "status": "SUCCESS", + "startTime": "2026-07-29T10:00:01Z", + "duration": "PT1.5S", + "output": [] + } + ], + "output": [] + }, + { + "groupId": "org.example", + "artifactId": "core", + "version": "1.0", + "status": "SUCCESS", + "startTime": "2026-07-29T10:00:03Z", + "duration": "PT3.4S", + "mojos": [], + "output": [] + } + ], + "problems": [ + { + "key": "deprecated-source-target", + "severity": "WARNING", + "message": "source/target value 8 is obsolete and will be removed in a future release", + "source": "maven-compiler-plugin:3.15.0:compile", + "suggestion": "Update maven.compiler.source to 11 or higher", + "documentationUrl": "https://maven.apache.org/plugins/maven-compiler-plugin/" + } + ], + "failures": [], + "output": [] + }"""; + return SimpleJsonReader.parse(json); + } + + private Map createFailureReport() { + String json = """ + { + "formatVersion": "1.0", + "status": "FAILURE", + "duration": "PT5.0S", + "startTime": "2026-07-29T10:00:00Z", + "mavenVersion": "4.1.0-SNAPSHOT", + "javaVersion": "21.0.1", + "goals": ["compile"], + "project": "org.example:root", + "multiModule": false, + "threads": 1, + "modules": [ + { + "groupId": "org.example", + "artifactId": "core", + "version": "1.0", + "status": "FAILURE", + "startTime": "2026-07-29T10:00:01Z", + "duration": "PT5.0S", + "mojos": [], + "output": [] + } + ], + "problems": [], + "failures": [ + { + "module": "org.example:core", + "mojo": "compiler:compile", + "timestamp": "2026-07-29T10:00:05Z", + "message": "Compilation failure", + "stackTrace": "org.apache.maven.lifecycle.LifecycleExecutionException\\nat Lifecycle.java:42" + } + ], + "output": [] + }"""; + return SimpleJsonReader.parse(json); + } + + @Test + void testRenderSummarySuccess() { + List lines = new ArrayList<>(); + BuildReportRenderer renderer = new BuildReportRenderer(messageBuilderFactory, lines::add); + + renderer.renderSummary(createSuccessReport()); + + String output = String.join("\n", lines); + assertTrue(output.contains("Build Report"), "Should contain header"); + assertTrue(output.contains("Maven 4.1.0-SNAPSHOT"), "Should contain Maven version"); + assertTrue(output.contains("BUILD SUCCESS"), "Should contain BUILD SUCCESS"); + assertTrue(output.contains("api"), "Should contain first module"); + assertTrue(output.contains("core"), "Should contain second module"); + assertTrue(output.contains("2 modules"), "Should contain module count"); + assertTrue(output.contains("2 passed"), "Should contain passed count"); + assertTrue(output.contains("1 warning"), "Should contain warning count"); + assertTrue(output.contains("source/target value 8"), "Should show actual warning message in default view"); + assertTrue(output.contains("maven-compiler-plugin"), "Should show warning source in default view"); + assertTrue(output.contains("Update maven.compiler.source"), "Should show suggestion in default view"); + assertTrue(output.contains("Total time: 6.700 s"), "Should contain formatted total time"); + } + + @Test + void testRenderSummaryFailure() { + List lines = new ArrayList<>(); + BuildReportRenderer renderer = new BuildReportRenderer(messageBuilderFactory, lines::add); + + renderer.renderSummary(createFailureReport()); + + String output = String.join("\n", lines); + assertTrue(output.contains("BUILD FAILURE"), "Should contain BUILD FAILURE"); + assertTrue(output.contains("1 failure"), "Should contain failure count"); + } + + @Test + void testRenderDiagnostics() { + List lines = new ArrayList<>(); + BuildReportRenderer renderer = new BuildReportRenderer(messageBuilderFactory, lines::add); + + renderer.renderDiagnostics(createSuccessReport()); + + String output = String.join("\n", lines); + assertTrue(output.contains("Problems (1)"), "Should contain problems header"); + assertTrue(output.contains("source/target value 8"), "Should contain warning message"); + assertTrue(output.contains("deprecated-source-target"), "Should contain problem key"); + assertTrue(output.contains("maven-compiler-plugin"), "Should contain source"); + assertTrue(output.contains("Update maven.compiler.source"), "Should contain suggestion"); + assertTrue( + output.contains("https://maven.apache.org/plugins/maven-compiler-plugin/"), + "Should contain documentation URL"); + } + + @Test + void testRenderDiagnosticsWhenEmpty() { + List lines = new ArrayList<>(); + BuildReportRenderer renderer = new BuildReportRenderer(messageBuilderFactory, lines::add); + + renderer.renderDiagnostics(createFailureReport()); + + String output = String.join("\n", lines); + assertTrue(output.contains("No problems recorded"), "Should show empty message"); + } + + @Test + void testRenderFailures() { + List lines = new ArrayList<>(); + BuildReportRenderer renderer = new BuildReportRenderer(messageBuilderFactory, lines::add); + + renderer.renderFailures(createFailureReport()); + + String output = String.join("\n", lines); + assertTrue(output.contains("Failures (1)"), "Should contain failures header"); + assertTrue(output.contains("org.example:core"), "Should contain module name"); + assertTrue(output.contains("compiler:compile"), "Should contain mojo"); + assertTrue(output.contains("Compilation failure"), "Should contain error message"); + } + + @Test + void testRenderFull() { + List lines = new ArrayList<>(); + BuildReportRenderer renderer = new BuildReportRenderer(messageBuilderFactory, lines::add); + + renderer.renderFull(createSuccessReport()); + + String output = String.join("\n", lines); + assertTrue(output.contains("Module: api"), "Should contain module name"); + assertTrue(output.contains("compiler"), "Should contain mojo plugin"); + assertTrue(output.contains("compile"), "Should contain mojo goal"); + assertTrue(output.contains("default-compile"), "Should contain execution id"); + } + + @Test + void testFormatDuration() { + assertEquals("6.700 s", BuildReportRenderer.formatDuration("PT6.7S")); + assertEquals("0.100 s", BuildReportRenderer.formatDuration("PT0.1S")); + assertEquals("1:30 min", BuildReportRenderer.formatDuration("PT1M30S")); + assertEquals("PT-invalid", BuildReportRenderer.formatDuration("PT-invalid")); // fallback + } +} diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnlog/SimpleJsonReaderTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnlog/SimpleJsonReaderTest.java new file mode 100644 index 000000000000..451151387cef --- /dev/null +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnlog/SimpleJsonReaderTest.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.cling.invoker.mvnlog; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SimpleJsonReaderTest { + + @Test + void testParseEmptyObject() { + Map result = SimpleJsonReader.parse("{}"); + assertTrue(result.isEmpty()); + } + + @Test + void testParseSimpleObject() { + Map result = SimpleJsonReader.parse(""" + {"name": "test", "version": "1.0"}"""); + assertEquals("test", result.get("name")); + assertEquals("1.0", result.get("version")); + } + + @Test + void testParseNumbers() { + Map result = SimpleJsonReader.parse(""" + {"count": 42, "ratio": 3.14, "negative": -7}"""); + assertEquals(42, result.get("count")); + assertEquals(3.14, result.get("ratio")); + assertEquals(-7, result.get("negative")); + } + + @Test + void testParseBooleanAndNull() { + Map result = SimpleJsonReader.parse(""" + {"active": true, "deleted": false, "extra": null}"""); + assertEquals(true, result.get("active")); + assertEquals(false, result.get("deleted")); + assertNull(result.get("extra")); + } + + @Test + @SuppressWarnings("unchecked") + void testParseArray() { + Map result = SimpleJsonReader.parse(""" + {"goals": ["clean", "install"]}"""); + List goals = (List) result.get("goals"); + assertEquals(2, goals.size()); + assertEquals("clean", goals.get(0)); + assertEquals("install", goals.get(1)); + } + + @Test + @SuppressWarnings("unchecked") + void testParseNestedObject() { + Map result = SimpleJsonReader.parse(""" + {"module": {"artifactId": "core", "status": "SUCCESS"}}"""); + Map module = (Map) result.get("module"); + assertEquals("core", module.get("artifactId")); + assertEquals("SUCCESS", module.get("status")); + } + + @Test + void testParseStringEscapes() { + Map result = SimpleJsonReader.parse(""" + {"msg": "line1\\nline2", "path": "C:\\\\Users"}"""); + assertEquals("line1\nline2", result.get("msg")); + assertEquals("C:\\Users", result.get("path")); + } + + @Test + @SuppressWarnings("unchecked") + void testParseBuildReportFragment() { + String json = """ + { + "formatVersion": "1.0", + "status": "SUCCESS", + "duration": "PT6.7S", + "mavenVersion": "4.1.0-SNAPSHOT", + "modules": [ + { + "artifactId": "maven-api-core", + "status": "SUCCESS", + "duration": "PT2.1S", + "mojos": [] + }, + { + "artifactId": "maven-core", + "status": "SUCCESS", + "duration": "PT3.4S", + "mojos": [] + } + ], + "problems": [], + "failures": [] + }"""; + + Map report = SimpleJsonReader.parse(json); + assertEquals("1.0", report.get("formatVersion")); + assertEquals("SUCCESS", report.get("status")); + assertEquals("PT6.7S", report.get("duration")); + + List> modules = (List>) (List) report.get("modules"); + assertEquals(2, modules.size()); + assertEquals("maven-api-core", modules.get(0).get("artifactId")); + assertEquals("maven-core", modules.get(1).get("artifactId")); + } + + @Test + void testParseInvalidJson() { + assertThrows(IllegalArgumentException.class, () -> SimpleJsonReader.parse("not json")); + } + + @Test + void testParseNonObjectRoot() { + assertThrows(IllegalArgumentException.class, () -> SimpleJsonReader.parse("[1, 2, 3]")); + } +} diff --git a/its/core-it-suite/pom.xml b/its/core-it-suite/pom.xml index 5871a3570f4e..c5b0802febba 100644 --- a/its/core-it-suite/pom.xml +++ b/its/core-it-suite/pom.xml @@ -519,7 +519,7 @@ under the License. - + @@ -749,7 +749,7 @@ under the License. - + @@ -804,7 +804,7 @@ under the License. - + diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh12571BuildReportTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh12571BuildReportTest.java new file mode 100644 index 000000000000..dbb19974cbbe --- /dev/null +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh12571BuildReportTest.java @@ -0,0 +1,423 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.maven.it; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Integration tests for the Build Report Foundation feature. + *

+ * Covers: build report JSON generation, console modes (plain, machine, verbose), + * warning mode, version info on failure, and the {@code mvnlog} viewer tool. + * + * @see gh-12571 + * @since 4.1.0 + */ +class MavenITgh12571BuildReportTest extends AbstractMavenIntegrationTestCase { + + // ------------------------------------------------------------------------- + // Build report JSON generation + // ------------------------------------------------------------------------- + + /** + * Verify that a successful single-module build produces a JSON report file + * containing the expected top-level fields. + */ + @Test + void testBuildReportJsonGenerated() throws Exception { + Path basedir = extractResources("gh-12571-build-report"); + + Verifier verifier = newVerifier(basedir); + verifier.setLogFileName("report-gen.txt"); + verifier.addCliArgument("--console=verbose"); + verifier.addCliArgument("validate"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + // Build report file must exist + Path reportFile = basedir.resolve("target/build-reports/build-report-latest.json"); + verifier.verifyFilePresent(reportFile); + + // Verify JSON structure + String json = Files.readString(reportFile); + assertTrue(json.contains("\"formatVersion\""), "Should contain formatVersion"); + assertTrue(json.contains("\"status\""), "Should contain status"); + assertTrue(json.contains("\"SUCCESS\""), "Status should be SUCCESS"); + assertTrue(json.contains("\"duration\""), "Should contain duration"); + assertTrue(json.contains("\"mavenVersion\""), "Should contain mavenVersion"); + assertTrue(json.contains("\"javaVersion\""), "Should contain javaVersion"); + assertTrue(json.contains("\"modules\""), "Should contain modules array"); + assertTrue(json.contains("\"problems\""), "Should contain problems array"); + assertTrue(json.contains("\"failures\""), "Should contain failures array"); + assertTrue(json.contains("\"build-report-test\""), "Should contain artifactId"); + } + + /** + * Verify that a multi-module build produces a JSON report with all modules listed. + */ + @Test + void testBuildReportMultiModule() throws Exception { + Path basedir = extractResources("gh-12571-multi-module"); + + Verifier verifier = newVerifier(basedir); + verifier.setLogFileName("multi-module.txt"); + verifier.addCliArgument("--console=verbose"); + verifier.addCliArgument("validate"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + Path reportFile = basedir.resolve("target/build-reports/build-report-latest.json"); + verifier.verifyFilePresent(reportFile); + + String json = Files.readString(reportFile); + assertTrue(json.contains("\"module-a\""), "Should contain module-a"); + assertTrue(json.contains("\"module-b\""), "Should contain module-b"); + assertTrue(json.contains("\"multi-module-parent\""), "Should contain parent"); + assertTrue(json.contains("\"multiModule\""), "Should contain multiModule flag"); + } + + // ------------------------------------------------------------------------- + // Console modes + // ------------------------------------------------------------------------- + + /** + * Verify that {@code --console=plain} produces compact output without + * the full mojo-level detail that verbose mode shows. + */ + @Test + void testConsolePlainMode() throws Exception { + Path basedir = extractResources("gh-12571-multi-module"); + + Verifier verifier = newVerifier(basedir); + verifier.setLogFileName("plain.txt"); + verifier.addCliArgument("--console=plain"); + verifier.addCliArgument("validate"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + // Plain mode should still show BUILD SUCCESS and Total time + verifier.verifyTextInLog("BUILD SUCCESS"); + verifier.verifyTextInLog("Total time:"); + + // Plain mode should NOT show the verbose "--- plugin:goal" lines + List lines = verifier.loadLines("plain.txt"); + boolean hasPluginLine = lines.stream().anyMatch(l -> l.matches(".*---.*:.*---.*")); + assertFalse(hasPluginLine, "Plain mode should not contain verbose mojo execution lines"); + } + + /** + * Verify that {@code --console=machine} produces JSON lines output + * with typed events. + */ + @Test + void testConsoleMachineMode() throws Exception { + Path basedir = extractResources("gh-12571-build-report"); + + Verifier verifier = newVerifier(basedir); + verifier.setLogFileName("machine.txt"); + verifier.addCliArgument("--console=machine"); + verifier.addCliArgument("validate"); + verifier.execute(); + + List lines = verifier.loadLines("machine.txt"); + + // Machine mode should produce JSON lines with "event" fields + boolean hasBuildStarted = lines.stream().anyMatch(l -> l.contains("\"event\":\"build.started\"")); + boolean hasBuildFinished = lines.stream().anyMatch(l -> l.contains("\"event\":\"build.finished\"")); + boolean hasModuleStarted = lines.stream().anyMatch(l -> l.contains("\"event\":\"module.started\"")); + boolean hasTimestamp = lines.stream().anyMatch(l -> l.contains("\"timestamp\"")); + + assertTrue(hasBuildStarted, "Should contain build.started event"); + assertTrue(hasBuildFinished, "Should contain build.finished event"); + assertTrue(hasModuleStarted, "Should contain module.started event"); + assertTrue(hasTimestamp, "Events should contain timestamps"); + + // build.finished should report SUCCESS + boolean hasSuccess = + lines.stream().anyMatch(l -> l.contains("\"event\":\"build.finished\"") && l.contains("\"SUCCESS\"")); + assertTrue(hasSuccess, "build.finished should report SUCCESS"); + } + + /** + * Verify that {@code --console=verbose} (the classic Maven output) includes + * the standard banner lines and mojo execution details. + */ + @Test + void testConsoleVerboseMode() throws Exception { + Path basedir = extractResources("gh-12571-build-report"); + + Verifier verifier = newVerifier(basedir); + verifier.setLogFileName("verbose.txt"); + verifier.addCliArgument("--console=verbose"); + verifier.addCliArgument("validate"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + verifier.verifyTextInLog("BUILD SUCCESS"); + verifier.verifyTextInLog("Total time:"); + // Verbose mode shows the horizontal rule separator + verifier.verifyTextInLog("------------------------------------------------------------------------"); + } + + /** + * Verify that when the {@code CI} environment variable is set, + * {@code --console=auto} resolves to plain mode (no verbose mojo lines). + */ + @Test + void testConsoleAutoDetectsCi() throws Exception { + Path basedir = extractResources("gh-12571-multi-module"); + + Verifier verifier = newVerifier(basedir); + verifier.setLogFileName("auto-ci.txt"); + verifier.setEnvironmentVariable("CI", "true"); + verifier.addCliArgument("--console=auto"); + verifier.addCliArgument("validate"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + verifier.verifyTextInLog("BUILD SUCCESS"); + + // In CI mode (plain), should NOT show verbose mojo execution lines + List lines = verifier.loadLines("auto-ci.txt"); + boolean hasPluginLine = lines.stream().anyMatch(l -> l.matches(".*---.*:.*---.*")); + assertFalse(hasPluginLine, "Auto mode with CI=true should not produce verbose mojo lines"); + } + + // ------------------------------------------------------------------------- + // Warning mode + // ------------------------------------------------------------------------- + + /** + * Verify that {@code --warning-mode=none} suppresses the diagnostic summary + * at the end of the build. + */ + @Test + void testWarningModeNone() throws Exception { + Path basedir = extractResources("gh-12571-build-report"); + + Verifier verifier = newVerifier(basedir); + verifier.setLogFileName("warn-none.txt"); + verifier.addCliArgument("--console=verbose"); + verifier.addCliArgument("--warning-mode=none"); + verifier.addCliArgument("validate"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + // With --warning-mode=none, the diagnostic summary line should not appear + verifier.verifyTextNotInLog("Diagnostics:"); + } + + // ------------------------------------------------------------------------- + // Version info on failure (MNG-7372) + // ------------------------------------------------------------------------- + + /** + * Verify that on BUILD FAILURE the Maven version and Java version + * are printed in the summary output. + */ + @Test + void testVersionInfoOnFailure() throws Exception { + Path basedir = extractResources("gh-12571-build-report"); + + Verifier verifier = newVerifier(basedir); + verifier.setLogFileName("fail-version.txt"); + verifier.addCliArgument("--console=verbose"); + // Invoke a non-existent goal to trigger a failure + verifier.addCliArgument("org.apache.maven.plugins:non-existent-plugin:1.0:goal"); + + boolean failed = false; + try { + verifier.execute(); + } catch (VerificationException e) { + failed = true; + } + + assertTrue(failed, "Build should have failed"); + verifier.verifyTextInLog("BUILD FAILURE"); + // The version info line should be present (e.g. "Maven 4.1.0-SNAPSHOT | Java 21.0.x") + verifier.verifyTextInLog("Maven"); + verifier.verifyTextInLog("Java"); + } + + // ------------------------------------------------------------------------- + // mvnlog viewer + // ------------------------------------------------------------------------- + + /** + * Verify that {@code mvnlog} displays a summary of the last build report + * after a successful build. + */ + @Test + void testMvnlogShowsSummary() throws Exception { + Path basedir = extractResources("gh-12571-build-report"); + + // Step 1: run a build to generate the report + Verifier buildVerifier = newVerifier(basedir); + buildVerifier.setLogFileName("build-for-mvnlog.txt"); + buildVerifier.addCliArgument("--console=verbose"); + buildVerifier.addCliArgument("validate"); + buildVerifier.execute(); + buildVerifier.verifyErrorFreeLog(); + + // Verify the report was generated + Path reportFile = basedir.resolve("target/build-reports/build-report-latest.json"); + buildVerifier.verifyFilePresent(reportFile); + + // Step 2: run mvnlog to view the report (forked: embedded executor does not know mvnlog) + Verifier logVerifier = newVerifier(basedir); + logVerifier.setLogFileName("mvnlog-summary.txt"); + logVerifier.setForkJvm(true); + logVerifier.setExecutable("mvnlog"); + logVerifier.execute(); + + // mvnlog should display report content + logVerifier.verifyTextInLog("Build Report"); + logVerifier.verifyTextInLog("SUCCESS"); + } + + /** + * Verify that {@code mvnlog --full} shows per-module detail. + */ + @Test + void testMvnlogFullView() throws Exception { + Path basedir = extractResources("gh-12571-multi-module"); + + // Step 1: run a build to generate the report + Verifier buildVerifier = newVerifier(basedir); + buildVerifier.setLogFileName("build-for-full.txt"); + buildVerifier.addCliArgument("--console=verbose"); + buildVerifier.addCliArgument("validate"); + buildVerifier.execute(); + buildVerifier.verifyErrorFreeLog(); + + // Step 2: run mvnlog --full (forked: embedded executor does not know mvnlog) + Verifier logVerifier = newVerifier(basedir); + logVerifier.setLogFileName("mvnlog-full.txt"); + logVerifier.setForkJvm(true); + logVerifier.setExecutable("mvnlog"); + logVerifier.addCliArgument("--full"); + logVerifier.execute(); + + // Full view should show individual module details + logVerifier.verifyTextInLog("module-a"); + logVerifier.verifyTextInLog("module-b"); + } + + /** + * Verify that {@code mvnlog} reports an error when no build report exists. + */ + @Test + void testMvnlogNoReport() throws Exception { + Path basedir = extractResources("gh-12571-build-report"); + + // Clean any prior reports + Path reportsDir = basedir.resolve("target/build-reports"); + if (Files.isDirectory(reportsDir)) { + Files.walk(reportsDir) + .sorted(java.util.Comparator.reverseOrder()) + .forEach(p -> { + try { + Files.deleteIfExists(p); + } catch (Exception e) { + // ignore + } + }); + } + + Verifier logVerifier = newVerifier(basedir); + logVerifier.setLogFileName("mvnlog-no-report.txt"); + logVerifier.setForkJvm(true); + logVerifier.setExecutable("mvnlog"); + + boolean failed = false; + try { + logVerifier.execute(); + } catch (VerificationException e) { + failed = true; + } + + assertTrue(failed, "mvnlog should fail when no report exists"); + logVerifier.verifyTextInLog("Build report not found"); + } + + /** + * Verify that {@code mvnlog --list} lists available reports. + */ + @Test + void testMvnlogListReports() throws Exception { + Path basedir = extractResources("gh-12571-build-report"); + + // Step 1: run a build to generate at least one report + Verifier buildVerifier = newVerifier(basedir); + buildVerifier.setLogFileName("build-for-list.txt"); + buildVerifier.addCliArgument("--console=verbose"); + buildVerifier.addCliArgument("validate"); + buildVerifier.execute(); + buildVerifier.verifyErrorFreeLog(); + + // Step 2: run mvnlog --list (forked: embedded executor does not know mvnlog) + Verifier logVerifier = newVerifier(basedir); + logVerifier.setLogFileName("mvnlog-list.txt"); + logVerifier.setForkJvm(true); + logVerifier.setExecutable("mvnlog"); + logVerifier.addCliArgument("--list"); + logVerifier.execute(); + + // Should list the report file(s) + logVerifier.verifyTextInLog("build-report"); + } + + /** + * Verify that {@code mvnlog --json} outputs the raw JSON build report, + * suitable for piping to tools like {@code jq}. + */ + @Test + void testMvnlogJsonOutput() throws Exception { + Path basedir = extractResources("gh-12571-build-report"); + + // Step 1: run a build to generate the report + Verifier buildVerifier = newVerifier(basedir); + buildVerifier.setLogFileName("build-for-json.txt"); + buildVerifier.addCliArgument("--console=verbose"); + buildVerifier.addCliArgument("validate"); + buildVerifier.execute(); + buildVerifier.verifyErrorFreeLog(); + + // Step 2: run mvnlog --json (forked: embedded executor does not know mvnlog) + Verifier logVerifier = newVerifier(basedir); + logVerifier.setLogFileName("mvnlog-json.txt"); + logVerifier.setForkJvm(true); + logVerifier.setExecutable("mvnlog"); + logVerifier.addCliArgument("--json"); + logVerifier.execute(); + + // Output should be valid JSON with expected fields + logVerifier.verifyTextInLog("\"formatVersion\""); + logVerifier.verifyTextInLog("\"status\""); + logVerifier.verifyTextInLog("\"modules\""); + logVerifier.verifyTextInLog("\"mavenVersion\""); + } +} diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5760ResumeFeatureTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5760ResumeFeatureTest.java index 010e39cc2d32..695fd825d615 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5760ResumeFeatureTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5760ResumeFeatureTest.java @@ -63,6 +63,7 @@ public void testShouldSuggestToResumeWithoutArgs() throws Exception { verifier.addCliArgument("-Dmodule-b.fail=true"); try { + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("test"); verifier.execute(); fail("Expected this invocation to fail"); @@ -74,6 +75,7 @@ public void testShouldSuggestToResumeWithoutArgs() throws Exception { // New build with -r should resume the build from module-b, skipping module-a since it has succeeded already. verifier = newVerifier(parentDependentTestDir); verifier.addCliArgument("-r"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("test"); verifier.execute(); verifier.verifyTextNotInLog("Building module-a 1.0"); @@ -88,6 +90,7 @@ public void testShouldSkipSuccessfulProjects() throws Exception { verifier.addCliArgument("--fail-at-end"); try { + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("test"); verifier.execute(); fail("Expected this invocation to fail"); @@ -102,6 +105,7 @@ public void testShouldSkipSuccessfulProjects() throws Exception { // ... but adding -r should exclude those two from the build because the previous Maven invocation // marked them as successfully built. verifier.addCliArgument("-r"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("test"); verifier.execute(); } @@ -116,6 +120,7 @@ public void testShouldSkipSuccessfulModulesWhenTheFirstModuleFailed() throws Exc verifier.addCliArgument("--fail-at-end"); try { + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("test"); verifier.execute(); fail("Expected this invocation to fail"); @@ -125,6 +130,7 @@ public void testShouldSkipSuccessfulModulesWhenTheFirstModuleFailed() throws Exc verifier = newVerifier(parentIndependentTestDir); verifier.addCliArgument("-r"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("test"); verifier.execute(); verifier.verifyTextInLog("Building module-a 1.0"); @@ -139,6 +145,7 @@ public void testShouldNotCrashWithoutProject() throws Exception { // https://issues.apache.org/jira/browse/MNG-5760?focusedCommentId=17143795&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-17143795) final Verifier verifier = newVerifier(noProjectTestDir); try { + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("org.apache.maven.plugins:maven-resources-plugin:resources"); verifier.execute(); } catch (final VerificationException ve) { @@ -164,6 +171,7 @@ public void testFailureWithParallelBuild() throws Exception { verifier.addCliArgument("-Dmodule-a.fail=true"); verifier.addCliArgument("-Dmodule-c.fail=true"); try { + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("verify"); verifier.execute(); fail("Expected this invocation to fail"); @@ -183,6 +191,7 @@ public void testFailureWithParallelBuild() throws Exception { // c : success // d : success + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("verify"); verifier.execute(); } @@ -204,6 +213,7 @@ public void testFailureAfterSkipWithParallelBuild() throws Exception { verifier.addCliArgument("-Dmodule-b.delay=2000"); verifier.addCliArgument("-Dmodule-d.fail=true"); try { + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("verify"); verifier.execute(); fail("Expected this invocation to fail"); @@ -223,6 +233,7 @@ public void testFailureAfterSkipWithParallelBuild() throws Exception { // The result should be: // c : success // d : success + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("verify"); verifier.execute(); } diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5965ParallelBuildMultipliesWorkTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5965ParallelBuildMultipliesWorkTest.java index 9fc554a24710..353944c61681 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5965ParallelBuildMultipliesWorkTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng5965ParallelBuildMultipliesWorkTest.java @@ -43,6 +43,7 @@ public void testItShouldOnlyRunEachTaskOnce() throws Exception { verifier.setLogFileName("log-only.txt"); verifier.addCliArgument("-T1"); // include an aggregator task so that the two goals end up in different task segments + verifier.addCliArgument("--console=verbose"); verifier.addCliArguments("clean", "install:help"); verifier.execute(); verifier.verifyErrorFreeLog(); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6057CheckReactorOrderTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6057CheckReactorOrderTest.java index 44019dd9c46b..c41109e26512 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6057CheckReactorOrderTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6057CheckReactorOrderTest.java @@ -56,6 +56,7 @@ public void testitReactorShouldResultInExpectedOrder() throws Exception { verifier.setLogFileName("log-only.txt"); verifier.addCliArgument("-Drevision=1.3.0-SNAPSHOT"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("clean"); verifier.execute(); verifier.verifyErrorFreeLog(); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6065FailOnSeverityTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6065FailOnSeverityTest.java index 65a53dde3287..807798e7ee2a 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6065FailOnSeverityTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6065FailOnSeverityTest.java @@ -45,6 +45,7 @@ public void testItShouldFailOnWarnLogMessages() throws Exception { boolean failed = false; try { + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("compile"); verifier.execute(); } catch (VerificationException e) { @@ -65,6 +66,7 @@ public void testItShouldSucceedOnWarnLogMessagesWhenFailLevelIsError() throws Ex verifier.addCliArgument("--fail-on-severity"); verifier.addCliArgument("ERROR"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("compile"); verifier.execute(); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6118SubmoduleInvocation.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6118SubmoduleInvocation.java index 6a8519034876..25545f868d4a 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6118SubmoduleInvocation.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6118SubmoduleInvocation.java @@ -56,6 +56,7 @@ public MavenITmng6118SubmoduleInvocation() throws IOException { public void testInSubModule() throws Exception { // Compile the whole project first. Verifier verifier = newVerifier(testDir.toString(), false); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("package"); verifier.execute(); @@ -63,6 +64,7 @@ public void testInSubModule() throws Exception { verifier = newVerifier(submoduleDirectory.toString(), false); verifier.setAutoclean(false); verifier.setLogFileName("log-insubmodule.txt"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("compile"); verifier.execute(); } @@ -76,6 +78,7 @@ public void testInSubModule() throws Exception { public void testWithFile() throws Exception { // Compile the whole project first. Verifier verifier = newVerifier(testDir.toString(), false); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("package"); verifier.execute(); @@ -84,6 +87,7 @@ public void testWithFile() throws Exception { verifier.setLogFileName("log-withfile.txt"); verifier.addCliArgument("-f"); verifier.addCliArgument("app/pom.xml"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("compile"); verifier.execute(); } @@ -100,6 +104,7 @@ public void testWithFileAndAlsoMake() throws Exception { verifier.addCliArgument("-f"); verifier.addCliArgument("app/pom.xml"); verifier.setLogFileName("log-withfilealsomake.txt"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("compile"); verifier.execute(); verifier.verifyTextInLog("Building Maven Integration Test :: MNG-6118 :: Library 1.0"); @@ -116,6 +121,7 @@ public void testInSubModuleWithAlsoMake() throws Exception { Verifier verifier = newVerifier(submoduleDirectory, false); verifier.addCliArgument("-am"); verifier.setLogFileName("log-insubmodulealsomake.txt"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("compile"); verifier.execute(); verifier.verifyTextInLog("Building Maven Integration Test :: MNG-6118 :: Library 1.0"); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6391PrintVersionTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6391PrintVersionTest.java index 89c39470d1c7..0cac3cd9930e 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6391PrintVersionTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6391PrintVersionTest.java @@ -54,6 +54,7 @@ public void testitShouldPrintVersionAtTopAndAtBottom() throws Exception { verifier.setAutoclean(false); verifier.setLogFileName("version-log.txt"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("clean"); verifier.execute(); verifier.verifyErrorFreeLog(); @@ -95,6 +96,7 @@ public void testitShouldPrintVersionInAllLines() throws Exception { verifier.setAutoclean(false); verifier.setLogFileName("version-log.txt"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArguments("clean"); verifier.execute(); verifier.verifyErrorFreeLog(); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6566ExecuteAnnotationShouldNotReExecuteGoalsTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6566ExecuteAnnotationShouldNotReExecuteGoalsTest.java index 3321e0b5d854..334f25084c63 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6566ExecuteAnnotationShouldNotReExecuteGoalsTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6566ExecuteAnnotationShouldNotReExecuteGoalsTest.java @@ -38,6 +38,7 @@ public void setUp() throws Exception { Path pluginDir = testDir.resolve("plugin"); Verifier verifier = newVerifier(pluginDir); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("install"); verifier.execute(); verifier.verifyErrorFreeLog(); @@ -49,6 +50,7 @@ public void testRunsCompileGoalOnceWithDirectPluginInvocation() throws Exception Verifier verifier = newVerifier(consumerDir); verifier.setLogFileName("log-direct-plugin-invocation.txt"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument(PLUGIN_KEY + ":require-compile-phase"); verifier.execute(); verifier.verifyErrorFreeLog(); @@ -68,6 +70,7 @@ public void testRunsCompileGoalOnceWithPhaseExecution() throws Exception { Verifier verifier = newVerifier(consumerDir); verifier.setLogFileName("log-phase-execution.txt"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("compile"); verifier.execute(); verifier.verifyErrorFreeLog(); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6981ProjectListShouldIncludeChildrenTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6981ProjectListShouldIncludeChildrenTest.java index ba6864f2e9a8..19cade9d559c 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6981ProjectListShouldIncludeChildrenTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6981ProjectListShouldIncludeChildrenTest.java @@ -33,6 +33,7 @@ public void testProjectListShouldIncludeChildrenByDefault() throws Exception { verifier.addCliArgument("-pl"); verifier.addCliArgument(":module-a"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("compile"); verifier.execute(); verifier.verifyTextInLog("Building module-a-1 1.0"); @@ -52,6 +53,7 @@ public void testFileSwitchAllowsExcludeOfChildren() throws Exception { verifier.addCliArgument("module-a"); verifier.addCliArgument("--non-recursive"); verifier.setLogFileName("log-non-recursive.txt"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("compile"); verifier.execute(); verifier.verifyTextNotInLog("Building module-a-1 1.0"); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng7353CliGoalInvocationTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng7353CliGoalInvocationTest.java index b37994fd1647..f825aeda4d34 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng7353CliGoalInvocationTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng7353CliGoalInvocationTest.java @@ -32,6 +32,7 @@ private void run(String id, String goal, String expectedInvocation) throws Excep Path basedir = extractResources("mng-7353-cli-goal-invocation"); Verifier verifier = newVerifier(basedir); verifier.setLogFileName(id + ".txt"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument(goal); verifier.execute(); verifier.verifyTextInLog("[INFO] --- " + expectedInvocation); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng7804PluginExecutionOrderTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng7804PluginExecutionOrderTest.java index 10188f23e74b..b5fedd0d5b41 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng7804PluginExecutionOrderTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng7804PluginExecutionOrderTest.java @@ -46,6 +46,7 @@ void testOrder() throws Exception { Path testDir = extractResources("mng-7804-plugin-execution-order"); Verifier verifier = newVerifier(testDir); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("clean"); verifier.execute(); verifier.verifyErrorFreeLog(); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8594AtFileTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8594AtFileTest.java index bd1a6a21dd68..718b28b639ad 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8594AtFileTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8594AtFileTest.java @@ -44,6 +44,7 @@ void testIt() throws Exception { verifier.addCliArgument("cmd.txt"); verifier.addCliArgument("-Dcolor1=green"); verifier.addCliArgument("-Dcolor2=blue"); + verifier.addCliArgument("--console=verbose"); verifier.addCliArgument("clean"); verifier.execute(); verifier.verifyErrorFreeLog(); diff --git a/its/core-it-suite/src/test/resources/gh-12571-build-report/pom.xml b/its/core-it-suite/src/test/resources/gh-12571-build-report/pom.xml new file mode 100644 index 000000000000..d859b406d932 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-12571-build-report/pom.xml @@ -0,0 +1,28 @@ + + + + 4.0.0 + org.apache.maven.its.gh12571 + build-report-test + 1.0 + Maven IT :: gh-12571 :: Build Report + diff --git a/its/core-it-suite/src/test/resources/gh-12571-multi-module/module-a/pom.xml b/its/core-it-suite/src/test/resources/gh-12571-multi-module/module-a/pom.xml new file mode 100644 index 000000000000..4e63f41a8a33 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-12571-multi-module/module-a/pom.xml @@ -0,0 +1,31 @@ + + + + 4.0.0 + + org.apache.maven.its.gh12571 + multi-module-parent + 1.0 + + module-a + Module A + diff --git a/its/core-it-suite/src/test/resources/gh-12571-multi-module/module-b/pom.xml b/its/core-it-suite/src/test/resources/gh-12571-multi-module/module-b/pom.xml new file mode 100644 index 000000000000..52f4965b05a1 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-12571-multi-module/module-b/pom.xml @@ -0,0 +1,31 @@ + + + + 4.0.0 + + org.apache.maven.its.gh12571 + multi-module-parent + 1.0 + + module-b + Module B + diff --git a/its/core-it-suite/src/test/resources/gh-12571-multi-module/pom.xml b/its/core-it-suite/src/test/resources/gh-12571-multi-module/pom.xml new file mode 100644 index 000000000000..b3cabd7d3953 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-12571-multi-module/pom.xml @@ -0,0 +1,33 @@ + + + + 4.0.0 + org.apache.maven.its.gh12571 + multi-module-parent + 1.0 + pom + Maven IT :: gh-12571 :: Multi Module + + module-a + module-b + +