Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#608: enhance error messages #653

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7bbea6e
#608: enhanced error messages
jan-vcapgemini Sep 11, 2024
131933a
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Sep 16, 2024
c3a6b1c
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Sep 19, 2024
68a695e
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Sep 23, 2024
19b17d0
#608: implemented requested changes
jan-vcapgemini Sep 23, 2024
7423297
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Sep 25, 2024
91d64ab
#608: Adjusted out and error format
jan-vcapgemini Sep 25, 2024
867e62e
#608: added execute permissions to dotnet
jan-vcapgemini Sep 25, 2024
a952d83
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Sep 27, 2024
707d300
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Sep 30, 2024
8b60102
#608: applied refactorings and fixes
jan-vcapgemini Oct 4, 2024
8aa538a
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Oct 4, 2024
7e89d36
#608: fixed test
jan-vcapgemini Oct 4, 2024
b786464
#608: fixed test
jan-vcapgemini Oct 4, 2024
11dfda2
#608: optimized Dotnet test
jan-vcapgemini Oct 7, 2024
88b1475
#608: fixed tests
jan-vcapgemini Oct 7, 2024
20bd7c6
#608: fixed Dotnet windows test
jan-vcapgemini Oct 7, 2024
7a0433a
#608: fixed tests
jan-vcapgemini Oct 8, 2024
4113983
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Oct 14, 2024
bbb4c89
#608: fixed tests
jan-vcapgemini Oct 15, 2024
5d7f6af
#608: fixed tests
jan-vcapgemini Oct 15, 2024
0380559
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Oct 15, 2024
6cacf03
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Oct 15, 2024
b1ef58e
#608: disabled tests
jan-vcapgemini Oct 17, 2024
e516604
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Oct 21, 2024
42f59f5
#608: implemented requested changes
jan-vcapgemini Oct 21, 2024
1e4b116
#608: fixed test
jan-vcapgemini Oct 21, 2024
68770c7
#608: implemented requested changes
jan-vcapgemini Oct 24, 2024
0a3e586
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Oct 24, 2024
39400c5
#608: added changes to changelog
jan-vcapgemini Oct 24, 2024
858ffca
#608: fixed tests
jan-vcapgemini Oct 28, 2024
3f1b8ef
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Oct 28, 2024
7f65aee
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Oct 29, 2024
c39829e
Update cli/src/test/java/com/devonfw/tools/ide/context/ProcessContext…
jan-vcapgemini Oct 30, 2024
c7f2c55
#608: implemented requested changes
jan-vcapgemini Oct 30, 2024
0603a20
Merge branch 'main' into feature/608-enhance-error-messages
jan-vcapgemini Oct 30, 2024
e95034a
#608: moved changelog entry to new version
jan-vcapgemini Oct 30, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE

Release with new features and bugfixes:

* https://github.com/devonfw/IDEasy/issues/608[#608]: Enhanced error messages. Now logs missing command output and error messages

The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/15?closed=1[milestone 2024.11.001].

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import com.devonfw.tools.ide.common.SystemPath;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.environment.VariableLine;
import com.devonfw.tools.ide.log.IdeSubLogger;
import com.devonfw.tools.ide.log.IdeLogLevel;
import com.devonfw.tools.ide.os.SystemInfoImpl;
import com.devonfw.tools.ide.os.WindowsPathSyntax;
import com.devonfw.tools.ide.util.FilenameUtil;
Expand Down Expand Up @@ -180,14 +180,16 @@ public ProcessResult run(ProcessMode processMode) {
}

int exitCode;

if (processMode.isBackground()) {
exitCode = ProcessResult.SUCCESS;
} else {
exitCode = process.waitFor();
}

ProcessResult result = new ProcessResultImpl(exitCode, out, err);
performLogOnError(result, exitCode, interpreter);

performLogging(result, exitCode, interpreter);

return result;

Expand Down Expand Up @@ -310,25 +312,29 @@ private String addExecutable(String exec, List<String> args) {
return interpreter;
}

private void performLogOnError(ProcessResult result, int exitCode, String interpreter) {
private void performLogging(ProcessResult result, int exitCode, String interpreter) {

if (!result.isSuccessful() && (this.errorHandling != ProcessErrorHandling.NONE)) {
String message = createCommandMessage(interpreter, " failed with exit code " + exitCode + "!");
if (this.errorHandling == ProcessErrorHandling.THROW_CLI) {
throw new CliProcessException(message, result);
} else if (this.errorHandling == ProcessErrorHandling.THROW_ERR) {
throw new IllegalStateException(message);
}
IdeSubLogger level;
IdeLogLevel ideLogLevel;
String message = createCommandMessage(interpreter, "\nfailed with exit code " + exitCode + "!");

if (this.errorHandling == ProcessErrorHandling.LOG_ERROR) {
level = this.context.error();
ideLogLevel = IdeLogLevel.ERROR;
} else if (this.errorHandling == ProcessErrorHandling.LOG_WARNING) {
level = this.context.warning();
ideLogLevel = IdeLogLevel.WARNING;
} else {
level = this.context.error();
ideLogLevel = IdeLogLevel.ERROR;
this.context.error("Internal error: Undefined error handling {}", this.errorHandling);
}
level.log(message);

context.level(ideLogLevel).log(message);
result.log(ideLogLevel, context);
hohwille marked this conversation as resolved.
Show resolved Hide resolved

if (this.errorHandling == ProcessErrorHandling.THROW_CLI) {
throw new CliProcessException(message, result);
} else if (this.errorHandling == ProcessErrorHandling.THROW_ERR) {
throw new IllegalStateException(message);
}
}
}

Expand Down
19 changes: 19 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/process/ProcessResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import java.util.List;

import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.log.IdeLogLevel;

/**
* Result of a {@link Process} execution.
*
Expand Down Expand Up @@ -58,4 +61,20 @@ default boolean isSuccessful() {
*/
List<String> getErr();

/**
* Logs output and error messages on the provided log level.
*
* @param level the {@link IdeLogLevel} to use e.g. IdeLogLevel.ERROR.
* @param context the {@link IdeContext} to use.
*/
void log(IdeLogLevel level, IdeContext context);

/**
* Logs output and error messages on the provided log level.
*
* @param outLevel the {@link IdeLogLevel} to use for {@link #getOut()}.
* @param context the {@link IdeContext} to use.
* @param errorLevel the {@link IdeLogLevel} to use for {@link #getErr()}.
*/
void log(IdeLogLevel outLevel, IdeContext context, IdeLogLevel errorLevel);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import java.util.Collections;
import java.util.List;
import java.util.Objects;

import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.log.IdeLogLevel;

/**
* Implementation of {@link ProcessResult}.
Expand All @@ -25,16 +29,8 @@ public ProcessResultImpl(int exitCode, List<String> out, List<String> err) {

super();
this.exitCode = exitCode;
if (out == null) {
this.out = Collections.emptyList();
} else {
this.out = out;
}
if (err == null) {
this.err = Collections.emptyList();
} else {
this.err = err;
}
this.out = Objects.requireNonNullElse(out, Collections.emptyList());
this.err = Objects.requireNonNullElse(err, Collections.emptyList());
}

@Override
Expand All @@ -55,4 +51,29 @@ public List<String> getErr() {
return this.err;
}

@Override
public void log(IdeLogLevel level, IdeContext context) {
log(level, context, level);
}

public void log(IdeLogLevel outLevel, IdeContext context, IdeLogLevel errorLevel) {

if (!this.out.isEmpty()) {
doLog(outLevel, this.out, context);
}
if (!this.err.isEmpty()) {
doLog(errorLevel, this.err, context);
}
}

private void doLog(IdeLogLevel level, List<String> lines, IdeContext context) {
for (String line : lines) {
// remove !MESSAGE from log message
if (line.startsWith("!MESSAGE ")) {
line = line.substring(9);
}
context.level(level).log(line);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.tool.LocalToolCommandlet;

/**
* {@link LocalToolCommandlet} for <a href="https://docs.microsoft.com/en-us/dotnet/core/tools/">dotnet</a>. The .NET CLI (Command Line Interface)
* cross-platform tool for building, running, and managing .NET applications.
*/
public class DotNet extends LocalToolCommandlet {

/**
Expand Down
15 changes: 2 additions & 13 deletions cli/src/main/java/com/devonfw/tools/ide/tool/eclipse/Eclipse.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import java.nio.channels.FileLock;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;

import com.devonfw.tools.ide.cli.CliException;
Expand Down Expand Up @@ -90,19 +89,9 @@ public void installPlugin(ToolPluginDescriptor plugin, Step step) {
}
}
}
log(IdeLogLevel.DEBUG, result.getOut());
log(IdeLogLevel.ERROR, result.getErr());
step.error("Failed to install plugin {} ({}): exit code was {}", plugin.name(), plugin.id(), result.getExitCode());
}

private void log(IdeLogLevel level, List<String> lines) {

for (String line : lines) {
if (line.startsWith("!MESSAGE ")) {
line = line.substring(9);
}
this.context.level(level).log(line);
}
result.log(IdeLogLevel.DEBUG, context, IdeLogLevel.ERROR);
step.error("Failed to install plugin {} ({}): exit code was {}", plugin.name(), plugin.id(), result.getExitCode());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.devonfw.tools.ide.context;

import com.devonfw.tools.ide.cli.CliProcessException;
import com.devonfw.tools.ide.log.IdeLogLevel;
import com.devonfw.tools.ide.process.ProcessContextImpl;
import com.devonfw.tools.ide.process.ProcessMode;
import com.devonfw.tools.ide.process.ProcessResult;
Expand All @@ -22,24 +22,11 @@ public ProcessContextTestImpl(IdeContext context) {

@Override
public ProcessResult run(ProcessMode processMode) {

ProcessResult result;
try {
result = super.run(ProcessMode.DEFAULT_CAPTURE);
logOutput(result);
return result;
} catch (CliProcessException e) {
logOutput(e.getProcessResult());
throw e;
}
}

private void logOutput(ProcessResult result) {
for (String out : result.getOut()) {
this.context.info(out);
}
for (String err : result.getErr()) {
this.context.error(err);
ProcessResult result = super.run(ProcessMode.DEFAULT_CAPTURE);
// this hack is still required to capture test script output
if (result.isSuccessful() && (processMode == ProcessMode.DEFAULT || processMode == ProcessMode.BACKGROUND)) {
result.log(IdeLogLevel.INFO, context);
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
*/
public class ProcessContextImplTest extends AbstractIdeContextTest {

private ProcessContextImpl processConttextUnderTest;
private ProcessContextImpl processContextUnderTest;

private Process processMock;

Expand All @@ -39,20 +39,20 @@ public void setUp() throws Exception {

this.mockProcessBuilder = mock(ProcessBuilder.class);
this.context = newContext(PROJECT_BASIC, null, false);
this.processConttextUnderTest = new ProcessContextImpl(this.context);
this.processContextUnderTest = new ProcessContextImpl(this.context);

Field field = ReflectionUtils.findFields(ProcessContextImpl.class, f -> f.getName().equals("processBuilder"),
ReflectionUtils.HierarchyTraversalMode.TOP_DOWN).get(0);

field.setAccessible(true);
field.set(this.processConttextUnderTest, this.mockProcessBuilder);
field.set(this.processContextUnderTest, this.mockProcessBuilder);
field.setAccessible(false);

// underTest needs executable
Field underTestExecutable = ReflectionUtils.findFields(ProcessContextImpl.class,
f -> f.getName().equals("executable"), ReflectionUtils.HierarchyTraversalMode.TOP_DOWN).get(0);
underTestExecutable.setAccessible(true);
underTestExecutable.set(this.processConttextUnderTest,
underTestExecutable.set(this.processContextUnderTest,
TEST_PROJECTS.resolve("_ide/software/nonExistingBinaryForTesting"));
underTestExecutable.setAccessible(false);

Expand All @@ -77,12 +77,12 @@ public void missingExecutableShouldThrowIllegalState() throws Exception {
Field underTestExecutable = ReflectionUtils.findFields(ProcessContextImpl.class,
f -> f.getName().equals("executable"), ReflectionUtils.HierarchyTraversalMode.TOP_DOWN).get(0);
underTestExecutable.setAccessible(true);
underTestExecutable.set(this.processConttextUnderTest, null);
underTestExecutable.set(this.processContextUnderTest, null);
underTestExecutable.setAccessible(false);

// act & assert
Exception exception = assertThrows(IllegalStateException.class, () -> {
this.processConttextUnderTest.run(ProcessMode.DEFAULT);
this.processContextUnderTest.run(ProcessMode.DEFAULT);
});

String actualMessage = exception.getMessage();
Expand All @@ -98,7 +98,7 @@ public void onSuccessfulProcessStartReturnSuccessResult() throws Exception {
when(this.processMock.waitFor()).thenReturn(ProcessResult.SUCCESS);

// act
ProcessResult result = this.processConttextUnderTest.run(ProcessMode.DEFAULT);
ProcessResult result = this.processContextUnderTest.run(ProcessMode.DEFAULT);

// assert
verify(this.mockProcessBuilder).redirectOutput(
Expand Down Expand Up @@ -126,7 +126,7 @@ public void enablingCaptureShouldRedirectAndCaptureStreamsCorrectly() throws Exc
when(this.processMock.getErrorStream()).thenReturn(errorStream);

// act
ProcessResult result = this.processConttextUnderTest.run(ProcessMode.DEFAULT_CAPTURE);
ProcessResult result = this.processContextUnderTest.run(ProcessMode.DEFAULT_CAPTURE);

// assert
verify(this.mockProcessBuilder).redirectOutput(
Expand All @@ -149,7 +149,7 @@ public void enablingBackgroundProcessShouldNotBeAwaitedAndShouldNotPassStreams(P
when(this.processMock.waitFor()).thenReturn(ProcessResult.SUCCESS);

// act
ProcessResult result = this.processConttextUnderTest.run(processMode);
ProcessResult result = this.processContextUnderTest.run(processMode);

// assert
if (processMode == ProcessMode.BACKGROUND) {
Expand Down Expand Up @@ -179,11 +179,11 @@ public void unsuccessfulProcessShouldThrowIllegalState() throws Exception {
// arrange
when(this.processMock.waitFor()).thenReturn(ProcessResult.TOOL_NOT_INSTALLED);

this.processConttextUnderTest.errorHandling(ProcessErrorHandling.THROW_ERR);
this.processContextUnderTest.errorHandling(ProcessErrorHandling.THROW_ERR);

// act & assert
assertThrows(IllegalStateException.class, () -> {
this.processConttextUnderTest.run(ProcessMode.DEFAULT);
this.processContextUnderTest.run(ProcessMode.DEFAULT_CAPTURE);
});

}
Expand All @@ -194,16 +194,33 @@ public void processWarningAndErrorShouldBeLogged(ProcessErrorHandling processErr

// arrange
when(this.processMock.waitFor()).thenReturn(ProcessResult.TOOL_NOT_INSTALLED);
this.processConttextUnderTest.errorHandling(processErrorHandling);
this.processContextUnderTest.errorHandling(processErrorHandling);
String expectedMessage = "failed with exit code 4!";
// act
this.processConttextUnderTest.run(ProcessMode.DEFAULT);
this.processContextUnderTest.run(ProcessMode.DEFAULT);

// assert
IdeLogLevel level = convertToIdeLogLevel(processErrorHandling);
assertThat(this.context).log(level).hasMessageContaining(expectedMessage);
}

@Test
public void enablingCaptureShouldRedirectAndCaptureStreamsWithErrorsCorrectly() {
// arrange
IdeTestContext context = newContext(PROJECT_BASIC, null, false);

// act
IllegalStateException thrown = assertThrows(IllegalStateException.class,
() -> context.newProcess().executable(TEST_RESOURCES.resolve("process-context").resolve("dotnet.sh")).run());

// assert
assertThat(thrown.getMessage()).contains("failed with exit code 2");
assertThat(context).log(IdeLogLevel.ERROR).hasMessageContaining("content to stdout");
assertThat(context).log(IdeLogLevel.ERROR).hasMessageContaining("more content to stdout");
assertThat(context).log(IdeLogLevel.ERROR).hasMessageContaining("error message to stderr");
assertThat(context).log(IdeLogLevel.ERROR).hasMessageContaining("another error message to stderr");
}

private IdeLogLevel convertToIdeLogLevel(ProcessErrorHandling processErrorHandling) {

return switch (processErrorHandling) {
Expand Down
Loading
Loading