-
Notifications
You must be signed in to change notification settings - Fork 24
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
#734: Improve Process Result Model #773
base: main
Are you sure you want to change the base?
Changes from 16 commits
e25575f
8e66739
5c3cccb
4d5a556
daf4b45
05879d1
292aa5b
41a42ce
3da75c7
540a9b0
c2b7021
4530860
05da168
88c8ef1
4700583
f0c5b9e
4daec09
4bbb603
af6add1
8491143
d7e176e
ffd6ced
4c5f98a
dce5d5a
39de3a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.devonfw.tools.ide.process; | ||
|
||
/** | ||
* Represent an output message that can be either from stdout or stderr. | ||
* | ||
* @param error A boolean flag that indicates whether the output message is from stdout or stderr | ||
* @param message A string containing the outout message | ||
*/ | ||
public record OutputMessage(boolean error, String message) { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -168,17 +168,16 @@ public ProcessResult run(ProcessMode processMode) { | |
|
||
this.processBuilder.command(args); | ||
|
||
List<String> out = null; | ||
List<String> err = null; | ||
List<OutputMessage> output = new ArrayList<>(); | ||
|
||
Process process = this.processBuilder.start(); | ||
|
||
try { | ||
if (processMode == ProcessMode.DEFAULT_CAPTURE) { | ||
CompletableFuture<List<String>> outFut = readInputStream(process.getInputStream()); | ||
CompletableFuture<List<String>> errFut = readInputStream(process.getErrorStream()); | ||
out = outFut.get(); | ||
err = errFut.get(); | ||
CompletableFuture<List<String>> outFut = readInputStream(process.getInputStream(), false, output); | ||
CompletableFuture<List<String>> errFut = readInputStream(process.getErrorStream(), true, output); | ||
outFut.get(); | ||
errFut.get(); | ||
} | ||
|
||
int exitCode; | ||
|
@@ -189,7 +188,7 @@ public ProcessResult run(ProcessMode processMode) { | |
exitCode = process.waitFor(); | ||
} | ||
|
||
ProcessResult result = new ProcessResultImpl(this.executable.getFileName().toString(), command, exitCode, out, err); | ||
ProcessResult result = new ProcessResultImpl(this.executable.getFileName().toString(), command, exitCode, output); | ||
|
||
performLogging(result, exitCode, interpreter); | ||
|
||
|
@@ -220,11 +219,19 @@ public ProcessResult run(ProcessMode processMode) { | |
* @param is {@link InputStream}. | ||
* @return {@link CompletableFuture}. | ||
*/ | ||
private static CompletableFuture<List<String>> readInputStream(InputStream is) { | ||
private static CompletableFuture<List<String>> readInputStream(InputStream is, boolean errorStream, List<OutputMessage> outputMessages) { | ||
|
||
return CompletableFuture.supplyAsync(() -> { | ||
|
||
try (InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr)) { | ||
|
||
String line; | ||
while ((line = br.readLine()) != null) { | ||
synchronized (outputMessages) { | ||
OutputMessage outputMessage = new OutputMessage(errorStream, line); | ||
outputMessages.add(outputMessage); | ||
} | ||
alfeilex marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
return br.lines().toList(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we try to read the lines again from the buffered reader after it has already been consumed to the end? |
||
} catch (Throwable e) { | ||
throw new RuntimeException("There was a problem while executing the program", e); | ||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -3,6 +3,7 @@ | |||||||||
import java.util.Collections; | ||||||||||
import java.util.List; | ||||||||||
import java.util.Objects; | ||||||||||
import java.util.stream.Collectors; | ||||||||||
|
||||||||||
import com.devonfw.tools.ide.cli.CliProcessException; | ||||||||||
import com.devonfw.tools.ide.context.IdeContext; | ||||||||||
|
@@ -23,23 +24,25 @@ public class ProcessResultImpl implements ProcessResult { | |||||||||
|
||||||||||
private final List<String> err; | ||||||||||
|
||||||||||
private final List<OutputMessage> outputMessages; | ||||||||||
|
||||||||||
/** | ||||||||||
* The constructor. | ||||||||||
* | ||||||||||
* @param executable the {@link #getExecutable() executable}. | ||||||||||
* @param command the {@link #getCommand() command}. | ||||||||||
* @param exitCode the {@link #getExitCode() exit code}. | ||||||||||
* @param out the {@link #getOut() out}. | ||||||||||
* @param err the {@link #getErr() err}. | ||||||||||
* @param outputMessages {@link #getOutputMessages() output Messages}. | ||||||||||
*/ | ||||||||||
public ProcessResultImpl(String executable, String command, int exitCode, List<String> out, List<String> err) { | ||||||||||
public ProcessResultImpl(String executable, String command, int exitCode, List<OutputMessage> outputMessages) { | ||||||||||
|
||||||||||
super(); | ||||||||||
this.executable = executable; | ||||||||||
this.command = command; | ||||||||||
this.exitCode = exitCode; | ||||||||||
this.out = Objects.requireNonNullElse(out, Collections.emptyList()); | ||||||||||
this.err = Objects.requireNonNullElse(err, Collections.emptyList()); | ||||||||||
this.outputMessages = Objects.requireNonNullElse(outputMessages, Collections.emptyList()); | ||||||||||
this.out = this.outputMessages.stream().filter(outputMessage -> !outputMessage.error()).map(OutputMessage::message).collect(Collectors.toList()); | ||||||||||
this.err = this.outputMessages.stream().filter(OutputMessage::error).map(OutputMessage::message).collect(Collectors.toList()); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small simplification:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||||||||||
} | ||||||||||
alfeilex marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
||||||||||
@Override | ||||||||||
|
@@ -72,6 +75,13 @@ public List<String> getErr() { | |||||||||
return this.err; | ||||||||||
} | ||||||||||
|
||||||||||
@Override | ||||||||||
public List<OutputMessage> getOutputMessages() { | ||||||||||
|
||||||||||
return outputMessages; | ||||||||||
|
||||||||||
} | ||||||||||
|
||||||||||
@Override | ||||||||||
public void log(IdeLogLevel level, IdeContext context) { | ||||||||||
log(level, context, level); | ||||||||||
|
@@ -80,11 +90,8 @@ public void log(IdeLogLevel level, IdeContext context) { | |||||||||
@Override | ||||||||||
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); | ||||||||||
if (!this.outputMessages.isEmpty()) { | ||||||||||
doLog(outLevel, getOutputMessages().stream().map(OutputMessage::message).collect(Collectors.toList()), context); | ||||||||||
} | ||||||||||
alfeilex marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
} | ||||||||||
|
||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,9 @@ | |
import java.time.LocalDateTime; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.stream.Collectors; | ||
|
||
import com.devonfw.tools.ide.process.OutputMessage; | ||
import com.devonfw.tools.ide.process.ProcessContext; | ||
import com.devonfw.tools.ide.process.ProcessErrorHandling; | ||
import com.devonfw.tools.ide.process.ProcessMode; | ||
|
@@ -21,26 +23,26 @@ public class ProcessContextGitMock implements ProcessContext { | |
|
||
private final List<String> arguments; | ||
|
||
private final List<String> errors; | ||
|
||
private final List<String> outs; | ||
private final List<OutputMessage> outputMessages; | ||
|
||
private final LocalDateTime now; | ||
|
||
private int exitCode; | ||
|
||
private final Path directory; | ||
|
||
private final List<String> outs; | ||
|
||
/** | ||
* @param directory the {@link Path} to the git repository. | ||
*/ | ||
public ProcessContextGitMock(Path directory) { | ||
|
||
this.arguments = new ArrayList<>(); | ||
this.errors = new ArrayList<>(); | ||
this.outs = new ArrayList<>(); | ||
this.outputMessages = new ArrayList<>(); | ||
this.exitCode = ProcessResult.SUCCESS; | ||
this.directory = directory; | ||
this.outs = new ArrayList<>(); | ||
this.now = LocalDateTime.now(); | ||
} | ||
|
||
|
@@ -61,19 +63,30 @@ public void setExitCode(int exitCode) { | |
} | ||
|
||
/** | ||
* @return the {@link List} of mocked error messages. | ||
* @return the {@link List} of mocked stderr messages. | ||
*/ | ||
public List<String> getErrors() { | ||
|
||
return errors; | ||
return this.outputMessages.stream().filter(OutputMessage::error).map(OutputMessage::message).collect(Collectors.toList()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will not work. Current code is calling Could you discuss with @jan-vcapgemini who IMHO created this class about the design? BTW: I just noticed that you already found this problem and had to update an existing test-case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you want a convenience method for adding faked out or err messages, you could still provide them here but delegating to the ProcessResult. Also this way we could even allow a setter for the mocked There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
|
||
} | ||
|
||
/** | ||
* @return the {@link List} of mocked out messages. | ||
* @return the {@link List} of mocked stdout messages. | ||
*/ | ||
public List<String> getOuts() { | ||
|
||
return outs; | ||
return this.outputMessages.stream().filter(output -> !output.error()).map(OutputMessage::message).collect(Collectors.toList()); | ||
|
||
} | ||
|
||
/** | ||
* @return the {@link List} of mocked {@link OutputMessage}. | ||
*/ | ||
public List<OutputMessage> getOutputMessages() { | ||
|
||
return this.outputMessages; | ||
|
||
} | ||
|
||
@Override | ||
|
@@ -139,7 +152,8 @@ public ProcessResult run(ProcessMode processMode) { | |
// part of git cleanup checks if a new directory 'new-folder' exists | ||
if (this.arguments.contains("ls-files")) { | ||
if (Files.exists(this.directory.resolve("new-folder"))) { | ||
this.outs.add("new-folder"); | ||
OutputMessage outputMessage = new OutputMessage(false, "new-folder"); | ||
this.outputMessages.add(outputMessage); | ||
this.exitCode = 0; | ||
} | ||
} | ||
|
@@ -181,7 +195,7 @@ public ProcessResult run(ProcessMode processMode) { | |
} | ||
} | ||
this.arguments.clear(); | ||
return new ProcessResultImpl("git", command.toString(), this.exitCode, this.outs, this.errors); | ||
return new ProcessResultImpl("git", command.toString(), this.exitCode, this.outputMessages); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -221,6 +221,18 @@ public void enablingCaptureShouldRedirectAndCaptureStreamsWithErrorsCorrectly() | |
assertThat(context).log(IdeLogLevel.ERROR).hasMessageContaining("another error message to stderr"); | ||
} | ||
|
||
@Test | ||
public void defaultCaptureShouldCaptureStreamsWithCorrectOrder() { | ||
// arrange | ||
IdeTestContext context = newContext(PROJECT_BASIC, null, false); | ||
|
||
// act | ||
context.newProcess().executable(TEST_RESOURCES.resolve("process-context").resolve("log-order.sh")).run(); | ||
|
||
// assert | ||
assertThat(context).log(IdeLogLevel.INFO).hasEntries("out1", "err1", "out2", "err2"); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After several runs, you will see that the order is not deterministic, probably because the choice of the first There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||
private IdeLogLevel convertToIdeLogLevel(ProcessErrorHandling processErrorHandling) { | ||
|
||
return switch (processErrorHandling) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
#!/bin/bash | ||
echo "out1" | ||
echo "err1" >&2 | ||
sleep 5 | ||
echo "out2" | ||
echo "err2" >&2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may create a DeadLock.Java default synchronization is very inefficient and we will have two threads continuously blocking each other.Consider using a concurrent collection.
I would suggest to use
ConcurrentLinkedQueue
for collecting theOutputMessage
s and when completed, convert to a regularList
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried using
ConcurrentLinkedQueue
, and at first, it worked fine. However, after running my test case several times, I noticed that the order sometimes changed, causingerr1
to appear first in the list of output messages.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OMG. Sorry, if I caused a mess here.
Lets have a look at this in January after vacation time.
Maybe we can resolve the problem and make it work deterministic and reliable...
In the worst case, we have to revert the Concurrent-Collection approach and need to go back to your original solution using
synchronized
(or better usingReentrantLock
).