Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependency-reduced-pom.xml
.project
.classpath
.settings/
.factorypath

# IDEA
.idea
Expand All @@ -31,4 +32,4 @@ nb-configuration.xml
.cache/

# https://ge.apache.org
.mvn/.gradle-enterprise
.mvn/.gradle-enterprise
4 changes: 2 additions & 2 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ If SDKMAN! supports your operating system, it is as easy as
$ sdk install mvnd
----

If you used the manual install in the past, please make sure that the settings in `~/.m2/mvnd.properties` still make
sense. With SDKMAN!, the `~/.m2/mvnd.properties` file is typically not needed at all, because both `JAVA_HOME` and
If you used the manual installation in the past, please make sure that the settings in `~/.m2/mvnd.properties` still
make sense. With SDKMAN!, the `~/.m2/mvnd.properties` file is typically not needed at all, because both `JAVA_HOME` and
`MVND_HOME` are managed by SDKMAN!.

=== Install using https://brew.sh/[Homebrew]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ public enum Environment {
*/
MVND_NO_MODEL_CACHE("mvnd.noModelCache", null, Boolean.FALSE, OptionType.BOOLEAN, Flags.OPTIONAL),

/**
* If <code>true</code> (default), mvnd shows live per-test progress on each project's worker line while
* Surefire/Failsafe run tests. Set to <code>false</code> to disable the feature entirely (nothing is injected
* into the surefire/failsafe configuration and no listener is registered).
*/
MVND_TEST_PROGRESS("mvnd.testProgress", null, Boolean.TRUE, OptionType.BOOLEAN, Flags.OPTIONAL),

/**
* If <code>true</code>, the daemon will be launched in debug mode with the following JVM argument:
* <code>-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000</code>; otherwise the debug argument is
Expand Down
121 changes: 119 additions & 2 deletions common/src/main/java/org/mvndaemon/mvnd/common/Message.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ public abstract class Message {
public static final int PRINT_ERR = 26;
public static final int REQUEST_INPUT = 27;
public static final int INPUT_DATA = 28;
/**
* Live per-test progress for a project's line while surefire/failsafe run.
* TODO: the daemon-side feed that emits this message is not implemented on mvnd-1.x yet; until it
* lands in a follow-up commit, the client render stays dormant (no test-progress suffix is shown).
*/
public static final int PROJECT_TEST_PROGRESS = 29;

final int type;

Expand All @@ -89,6 +95,8 @@ public static Message read(DataInputStream input) throws IOException {
case PROJECT_LOG_MESSAGE:
case DISPLAY:
return ProjectEvent.read(type, input);
case PROJECT_TEST_PROGRESS:
return ProjectTestProgressEvent.read(input);
case BUILD_EXCEPTION:
return BuildException.read(input);
case KEEP_ALIVE:
Expand Down Expand Up @@ -145,6 +153,7 @@ public static int getClassOrder(Message m) {
case PROMPT:
case PROMPT_RESPONSE:
case DISPLAY:
case PROJECT_TEST_PROGRESS:
case PRINT_OUT:
case PRINT_ERR:
case REQUEST_INPUT:
Expand Down Expand Up @@ -234,6 +243,17 @@ static Map<String, String> readStringMap(DataInputStream input) throws IOExcepti
private static final int UTF_BUFS_BYTE_CNT = UTF_BUFS_CHAR_CNT * 3;
private static final ThreadLocal<byte[]> BUF_TLS = ThreadLocal.withInitial(() -> new byte[UTF_BUFS_BYTE_CNT]);

static void writeNullableUTF(DataOutputStream output, String value) throws IOException {
output.writeBoolean(value != null);
if (value != null) {
writeUTF(output, value);
}
}

static String readNullableUTF(DataInputStream input) throws IOException {
return input.readBoolean() ? readUTF(input) : null;
}

static String readUTF(DataInputStream input) throws IOException {
byte[] byteBuf = BUF_TLS.get();
int len = input.readInt();
Expand Down Expand Up @@ -615,6 +635,103 @@ public void write(DataOutputStream output) throws IOException {
}
}

public static class ProjectTestProgressEvent extends Message {
final String projectId;
final String testClass;
final String testMethod;
final int completed;
final int failures;
final int errors;
final int skipped;

public static ProjectTestProgressEvent read(DataInputStream input) throws IOException {
final String projectId = readUTF(input);
final String testClass = readNullableUTF(input);
final String testMethod = readNullableUTF(input);
final int completed = input.readInt();
final int failures = input.readInt();
final int errors = input.readInt();
final int skipped = input.readInt();
return new ProjectTestProgressEvent(projectId, testClass, testMethod, completed, failures, errors, skipped);
}

public ProjectTestProgressEvent(
String projectId,
String testClass,
String testMethod,
int completed,
int failures,
int errors,
int skipped) {
super(PROJECT_TEST_PROGRESS);
this.projectId = Objects.requireNonNull(projectId, "projectId cannot be null");
this.testClass = testClass;
this.testMethod = testMethod;
this.completed = completed;
this.failures = failures;
this.errors = errors;
this.skipped = skipped;
}

public String getProjectId() {
return projectId;
}

public String getTestClass() {
return testClass;
}

public String getTestMethod() {
return testMethod;
}

public int getCompleted() {
return completed;
}

public int getFailures() {
return failures;
}

public int getErrors() {
return errors;
}

public int getSkipped() {
return skipped;
}

@Override
public void write(DataOutputStream output) throws IOException {
super.write(output);
writeUTF(output, projectId);
writeNullableUTF(output, testClass);
writeNullableUTF(output, testMethod);
output.writeInt(completed);
output.writeInt(failures);
output.writeInt(errors);
output.writeInt(skipped);
}

@Override
public String toString() {
return "ProjectTestProgress{projectId='" + projectId + "', testClass='" + testClass + "', testMethod='"
+ testMethod + "', completed=" + completed + ", failures=" + failures + ", errors=" + errors
+ ", skipped=" + skipped + "}";
}
}

public static ProjectTestProgressEvent projectTestProgress(
String projectId,
String testClass,
String testMethod,
int completed,
int failures,
int errors,
int skipped) {
return new ProjectTestProgressEvent(projectId, testClass, testMethod, completed, failures, errors, skipped);
}

public static class BuildStarted extends Message {

final String projectId;
Expand Down Expand Up @@ -993,8 +1110,8 @@ public String toString() {
+ repositoryUrl + '\'' + ", resourceName='"
+ resourceName + '\'' + ", contentLength="
+ contentLength + ", transferredBytes="
+ transferredBytes + ", exception='"
+ exception + '\'' + '}';
+ transferredBytes
+ (exception != null ? ", exception='" + exception + '\'' : "") + '}';
}

private String mnemonic() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ public class TerminalOutput implements ClientOutput {

private static final AttributedStyle GREEN_FOREGROUND = new AttributedStyle().foreground(AttributedStyle.GREEN);
private static final AttributedStyle CYAN_FOREGROUND = new AttributedStyle().foreground(AttributedStyle.CYAN);
private static final AttributedStyle BOLD_GREEN_FOREGROUND =
new AttributedStyle().bold().foreground(AttributedStyle.GREEN);

private final Terminal terminal;
private final Terminal.SignalHandler previousIntHandler;
Expand Down Expand Up @@ -147,6 +149,7 @@ public class TerminalOutput implements ClientOutput {
static class Project {
final String id;
MojoStartedEvent runningExecution;
Message.ProjectTestProgressEvent testProgress;
final List<String> log = new ArrayList<>();

public Project(String id) {
Expand Down Expand Up @@ -279,6 +282,7 @@ private boolean doAccept(Message entry) {
final MojoStartedEvent execution = (MojoStartedEvent) entry;
final Project prj = projects.computeIfAbsent(execution.getArtifactId(), Project::new);
prj.runningExecution = execution;
prj.testProgress = null;
break;
}
case Message.PROJECT_STOPPED: {
Expand Down Expand Up @@ -463,6 +467,14 @@ private boolean doAccept(Message entry) {
daemonDispatch.accept(entry);
break;
}
case Message.PROJECT_TEST_PROGRESS: {
final Message.ProjectTestProgressEvent e = (Message.ProjectTestProgressEvent) entry;
final Project prj = projects.get(e.getProjectId());
if (prj != null) {
prj.testProgress = e;
}
break;
}
default:
throw new IllegalStateException("Unexpected message " + entry);
}
Expand Down Expand Up @@ -626,8 +638,16 @@ private void update() {
lines.addAll(logs);
remLogLines -= logs.size();
}
while (remLogLines-- > 0 && lines.size() <= maxThreads + 1) {
lines.add(AttributedString.EMPTY);
final AttributedString idleLine = new AttributedStringBuilder()
.style(BOLD_GREEN_FOREGROUND)
.append("> ")
.style(AttributedStyle.DEFAULT.faint())
.append("IDLE")
.style(AttributedStyle.DEFAULT)
.toAttributedString();
int idleSlots = maxThreads - projectsCount;
while (idleSlots-- > 0 && remLogLines-- > 0 && lines.size() <= maxThreads + 1) {
lines.add(idleLine);
}
} else {
int skipProjects = projectsCount - dispLines;
Expand Down Expand Up @@ -753,10 +773,41 @@ public static String pathToMaven(String location) {
return location;
}

static String renderBar(int percent) {
final int width = 20;
int filled = (int) Math.round(percent / 100.0 * width);
StringBuilder sb = new StringBuilder(width + 2);
sb.append('[');
if (filled >= width) {
for (int i = 0; i < width; i++) {
sb.append('=');
}
} else if (filled > 0) {
for (int i = 0; i < filled - 1; i++) {
sb.append('=');
}
sb.append('>');
for (int i = 0; i < width - filled; i++) {
sb.append(' ');
}
} else {
for (int i = 0; i < width; i++) {
sb.append(' ');
}
}
sb.append(']');
return sb.toString();
}

private void addStatusLine(final List<AttributedString> lines, int dispLines, final int projectsCount) {
if (name != null || buildStatus != null) {
AttributedStringBuilder asb = new AttributedStringBuilder();
if (name != null) {
int percent = doneProjects * 100 / totalProjects;
asb.append(renderBar(percent))
.append(' ')
.append(String.format("%3d", percent))
.append("% ");
asb.append("Building ");
asb.style(AttributedStyle.BOLD);
asb.append(name);
Expand Down Expand Up @@ -788,9 +839,6 @@ private void addStatusLine(final List<AttributedString> lines, int dispLines, fi
.append(String.format(projectsDoneFomat, doneProjects))
.append('/')
.append(String.valueOf(totalProjects))
.append(' ')
.append(String.format("%3d", doneProjects * 100 / totalProjects))
.append('%')
.style(AttributedStyle.DEFAULT);

} else if (buildStatus != null) {
Expand All @@ -811,38 +859,66 @@ private void addStatusLine(final List<AttributedString> lines, int dispLines, fi
private void addProjectLine(final List<AttributedString> lines, Project prj) {
final MojoStartedEvent execution = prj.runningExecution;
final AttributedStringBuilder asb = new AttributedStringBuilder();
asb.style(BOLD_GREEN_FOREGROUND).append("> ").style(AttributedStyle.DEFAULT);
AttributedString transfer = formatTransfers(prj.id);
if (transfer != null) {
asb.append(':')
.style(CYAN_FOREGROUND)
asb.style(CYAN_FOREGROUND)
.append(':')
.append(String.format(artifactIdFormat, prj.id))
.style(AttributedStyle.DEFAULT)
.append(transfer);
} else if (execution == null) {
asb.append(':').style(CYAN_FOREGROUND).append(prj.id);
asb.style(CYAN_FOREGROUND).append(':').append(prj.id).style(AttributedStyle.DEFAULT);
} else {
asb.append(':')
.style(CYAN_FOREGROUND)
asb.style(CYAN_FOREGROUND)
.append(':')
.append(String.format(artifactIdFormat, prj.id))
.style(GREEN_FOREGROUND);
if (execution.getPluginGoalPrefix().isEmpty()) {
asb.append(execution.getPluginGroupId()).append(':').append(execution.getPluginArtifactId());
asb.append(execution.getPluginArtifactId());
} else {
asb.append(execution.getPluginGoalPrefix());
}
asb.append(':')
.append(execution.getPluginVersion())
.append(':')
.append(execution.getMojo())
.append(' ')
.style(AttributedStyle.DEFAULT)
.append('(')
.append(execution.getExecutionId())
.append(')');
final Message.ProjectTestProgressEvent tp = prj.testProgress;
if (tp != null) {
appendTestProgress(asb, tp);
}
}
lines.add(asb.toAttributedString());
}

static void appendTestProgress(AttributedStringBuilder asb, Message.ProjectTestProgressEvent tp) {
final AttributedStyle faint = AttributedStyle.DEFAULT.faint();
final AttributedStyle red = AttributedStyle.DEFAULT.foreground(AttributedStyle.RED);
asb.append(' ').style(faint).append("[Tests: ").append(String.valueOf(tp.getCompleted()));
if (tp.getFailures() > 0) {
asb.style(faint).append(", Failures: ").style(red).append(String.valueOf(tp.getFailures()));
}
if (tp.getErrors() > 0) {
asb.style(faint).append(", Errors: ").style(red).append(String.valueOf(tp.getErrors()));
}
if (tp.getSkipped() > 0) {
asb.style(faint).append(", Skipped: ").append(String.valueOf(tp.getSkipped()));
}
asb.style(faint).append("]");
final String testClass = tp.getTestClass();
if (testClass != null) {
final String simple = testClass.substring(testClass.lastIndexOf('.') + 1);
asb.append(' ').append(simple);
if (tp.getTestMethod() != null) {
asb.append('#').append(tp.getTestMethod());
}
}
asb.style(AttributedStyle.DEFAULT);
}

private static <T> List<T> lastN(List<T> list, int n) {
return list.subList(Math.max(0, list.size() - n), list.size());
}
Expand Down
Loading
Loading