diff options
author | Håkon Hallingstad <hakon@oath.com> | 2018-02-06 11:49:16 +0100 |
---|---|---|
committer | Håkon Hallingstad <hakon@oath.com> | 2018-02-06 11:49:16 +0100 |
commit | c1b9b80f7fc5d0181362f4684fcac7454368c040 (patch) | |
tree | 4465847e31a901a3de8f31b31bb85e07cdb7cebf | |
parent | cfde440c54ab9f5ed8943cf12e39a0431f2043a0 (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);
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 |