summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHåkon Hallingstad <hakon@oath.com>2018-02-06 11:49:16 +0100
committerHåkon Hallingstad <hakon@oath.com>2018-02-06 11:49:16 +0100
commitc1b9b80f7fc5d0181362f4684fcac7454368c040 (patch)
tree4465847e31a901a3de8f31b31bb85e07cdb7cebf
parentcfde440c54ab9f5ed8943cf12e39a0431f2043a0 (diff)
Amend program output parse exceptions with command and output snippet
By using the CommandResult::map method, any exception thrown while parsing the output will automatically be wrapped in an exception that also dumps the command and (snippet of) the full output. It also facilitates simpler code, e.g.: List<String> volumeGroups = terminal.newCommandLine(context) .addTokens("vgs --noheadings --options vg_name") .executeSilently() .mapEachLine(String::trim);
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/ChildProcessException.java17
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLine.java8
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandResult.java41
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/TerminalImpl.java1
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/UnexpectedOutputException2.java10
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLineTest.java32
6 files changed, 106 insertions, 3 deletions
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/ChildProcessException.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/ChildProcessException.java
index b84bd2d8fef..9f7aaab2060 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/ChildProcessException.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/ChildProcessException.java
@@ -26,13 +26,26 @@ public abstract class ChildProcessException extends RuntimeException {
* @param possiblyHugeOutput The output of the command
*/
protected ChildProcessException(String problem, String commandLine, String possiblyHugeOutput) {
- super(makeSnippet(
+ super(makeSnippet(problem, commandLine, possiblyHugeOutput));
+ }
+
+ protected ChildProcessException(RuntimeException cause,
+ String problem,
+ String commandLine,
+ String possiblyHugeOutput) {
+ super(makeSnippet(problem, commandLine, possiblyHugeOutput), cause);
+ }
+
+ private static String makeSnippet(String problem,
+ String commandLine,
+ String possiblyHugeOutput) {
+ return makeSnippet(
problem,
commandLine,
possiblyHugeOutput,
MAX_OUTPUT_PREFIX,
MAX_OUTPUT_SUFFIX,
- MAX_OUTPUT_SLACK));
+ MAX_OUTPUT_SLACK);
}
// Package-private instead of private for testing.
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLine.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLine.java
index 6c4de7ac1e3..9a8ed19e0ac 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLine.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLine.java
@@ -9,6 +9,7 @@ import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
@@ -66,11 +67,16 @@ public class CommandLine {
public CommandLine add(String... arguments) { return add(Arrays.asList(arguments)); }
/** Add arguments to the command. The first argument in the first call to add() is the program. */
- public CommandLine add(List<String> arguments) {
+ public CommandLine add(Collection<String> arguments) {
this.arguments.addAll(arguments);
return this;
}
+ /** Add arguments by splitting arguments by space. */
+ public CommandLine addTokens(String arguments) {
+ return add(arguments.split(" "));
+ }
+
/**
* Execute a shell-like program in a child process:
* - the program is recorded and logged as modifying the system, but see executeSilently().
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandResult.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandResult.java
index 12f0d546b36..9905509e2d5 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandResult.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandResult.java
@@ -3,6 +3,7 @@
package com.yahoo.vespa.hosted.node.admin.task.util.process;
import java.util.List;
+import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -19,6 +20,8 @@ public class CommandResult {
private final int exitCode;
private final String output;
+ private boolean inMapFunction = false;
+
CommandResult(CommandLine commandLine, int exitCode, String output) {
this.commandLine = commandLine;
this.exitCode = exitCode;
@@ -39,6 +42,7 @@ public class CommandResult {
return getOutputLinesStream().collect(Collectors.toList());
}
+ /** Returns the output lines as a stream, omitting trailing empty lines. */
public Stream<String> getOutputLinesStream() {
if (output.isEmpty()) {
// For some reason an empty string => one-element list.
@@ -50,6 +54,43 @@ public class CommandResult {
}
/**
+ * Map this CommandResult to an instance of type R.
+ *
+ * If a RuntimeException is thrown by the mapper, it is wrapped in an
+ * UnexpectedOutputException2 that includes a snippet of the output in the message.
+ *
+ * This method is intended to be used as part of the verification of the output.
+ */
+ public <R> R map(Function<CommandResult, R> mapper) {
+ if (inMapFunction) {
+ throw new IllegalStateException("map() cannot be called recursively");
+ }
+ inMapFunction = true;
+
+ try {
+ return mapper.apply(this);
+ } catch (RuntimeException e) {
+ throw new UnexpectedOutputException2(e, "Failed to map output", commandLine.toString(), output);
+ } finally {
+ inMapFunction = false;
+ }
+ }
+
+ /**
+ * Map the output to an instance of type R according to mapper, wrapping any
+ * RuntimeException in UnexpectedOutputException2 w/output snippet. See map() for details.
+ */
+ public <R> R mapOutput(Function<String, R> mapper) { return map(result -> mapper.apply(result.getOutput())); }
+
+ /**
+ * Map each output line to an instance of type R according to mapper, wrapping any
+ * RuntimeException in UnexpectedOutputException2 w/output snippet. See map() for details.
+ */
+ public <R> List<R> mapEachLine(Function<String, R> mapper) {
+ return map(result -> result.getOutputLinesStream().map(mapper).collect(Collectors.toList()));
+ }
+
+ /**
* Convenience method for getting the CommandLine, whose execution resulted in
* this CommandResult instance.
*
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/TerminalImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/TerminalImpl.java
index 422584b8ccd..8ec0d267f0d 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/TerminalImpl.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/TerminalImpl.java
@@ -19,6 +19,7 @@ public class TerminalImpl implements Terminal {
this.processFactory = processFactory;
}
+ @Override
public CommandLine newCommandLine(TaskContext taskContext) {
return new CommandLine(taskContext, processFactory);
}
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/UnexpectedOutputException2.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/UnexpectedOutputException2.java
index e786452c0ef..82fae1aa70e 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/UnexpectedOutputException2.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/process/UnexpectedOutputException2.java
@@ -13,4 +13,14 @@ public class UnexpectedOutputException2 extends ChildProcessException {
public UnexpectedOutputException2(String problem, String commandLine, String possiblyHugeOutput) {
super("output was not of the expected format: " + problem, commandLine, possiblyHugeOutput);
}
+
+ /**
+ * @param problem Problem description, e.g. "Output is not of the form ^NAME=VALUE$"
+ */
+ public UnexpectedOutputException2(RuntimeException cause,
+ String problem,
+ String commandLine,
+ String possiblyHugeOutput) {
+ super(cause, "output was not of the expected format: " + problem, commandLine, possiblyHugeOutput);
+ }
}
diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLineTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLineTest.java
index dd6d9c8226d..397380461d6 100644
--- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLineTest.java
+++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/process/CommandLineTest.java
@@ -49,6 +49,9 @@ public class CommandLineTest {
List<CommandLine> commandLines = terminal.getTestProcessFactory().getMutableCommandLines();
assertEquals(1, commandLines.size());
assertTrue(commandLine == commandLines.get(0));
+
+ int lines = result.map(r -> r.getOutputLines().size());
+ assertEquals(2, lines);
}
@Test
@@ -112,4 +115,33 @@ public class CommandLineTest {
e.getMessage());
}
}
+
+ @Test
+ public void mapException() {
+ terminal.ignoreCommand("output");
+ CommandResult result = terminal.newCommandLine(context).add("program").execute();
+ IllegalArgumentException exception = new IllegalArgumentException("foo");
+ try {
+ result.mapOutput(output -> { throw exception; });
+ fail();
+ } catch (UnexpectedOutputException2 e) {
+ assertEquals("Command 'program 2>&1' output was not of the expected format: " +
+ "Failed to map output: stdout/stderr: 'output'", e.getMessage());
+ assertTrue(e.getCause() == exception);
+ }
+ }
+
+ @Test
+ public void testMapEachLine() {
+ assertEquals(
+ 1 + 2 + 3,
+ terminal.ignoreCommand("1\n2\n3\n")
+ .newCommandLine(context)
+ .add("foo")
+ .execute()
+ .mapEachLine(Integer::valueOf)
+ .stream()
+ .mapToInt(i -> i)
+ .sum());
+ }
} \ No newline at end of file