From 5e956429169d3a733114e5f76f051167f291c786 Mon Sep 17 00:00:00 2001 From: Morten Tokle Date: Tue, 7 Dec 2021 12:52:42 +0100 Subject: Extract vespa-feed-client-api module from vespa-feed-client --- vespa-feed-client-cli/pom.xml | 2 +- .../java/ai/vespa/feed/client/CliArguments.java | 338 --------------------- .../main/java/ai/vespa/feed/client/CliClient.java | 200 ------------ .../ai/vespa/feed/client/impl/CliArguments.java | 338 +++++++++++++++++++++ .../java/ai/vespa/feed/client/impl/CliClient.java | 207 +++++++++++++ .../src/main/sh/vespa-feed-client-standalone.sh | 2 +- .../src/main/sh/vespa-feed-client.sh | 2 +- .../ai/vespa/feed/client/CliArgumentsTest.java | 84 ----- .../vespa/feed/client/impl/CliArgumentsTest.java | 85 ++++++ 9 files changed, 633 insertions(+), 625 deletions(-) delete mode 100644 vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/CliArguments.java delete mode 100644 vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/CliClient.java create mode 100644 vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java create mode 100644 vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliClient.java delete mode 100644 vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/CliArgumentsTest.java create mode 100644 vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliArgumentsTest.java (limited to 'vespa-feed-client-cli') diff --git a/vespa-feed-client-cli/pom.xml b/vespa-feed-client-cli/pom.xml index aff625fe3a4..16d6f8827f2 100644 --- a/vespa-feed-client-cli/pom.xml +++ b/vespa-feed-client-cli/pom.xml @@ -74,7 +74,7 @@ false - ai.vespa.feed.client.CliClient + ai.vespa.feed.client.impl.CliClient diff --git a/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/CliArguments.java b/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/CliArguments.java deleted file mode 100644 index 0de81d2de36..00000000000 --- a/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/CliArguments.java +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package ai.vespa.feed.client; - -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.CommandLineParser; -import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.HelpFormatter; -import org.apache.commons.cli.Option; -import org.apache.commons.cli.Options; -import org.apache.commons.cli.ParseException; - -import java.io.File; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Path; -import java.time.Duration; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.OptionalDouble; -import java.util.OptionalInt; - -/** - * Parses command line arguments - * - * @author bjorncs - */ -class CliArguments { - - private static final Options optionsDefinition = createOptions(); - - private static final String BENCHMARK_OPTION = "benchmark"; - private static final String CA_CERTIFICATES_OPTION = "ca-certificates"; - private static final String CERTIFICATE_OPTION = "certificate"; - private static final String CONNECTIONS_OPTION = "connections"; - private static final String DISABLE_SSL_HOSTNAME_VERIFICATION_OPTION = "disable-ssl-hostname-verification"; - private static final String DRYRUN_OPTION = "dryrun"; - private static final String ENDPOINT_OPTION = "endpoint"; - private static final String FILE_OPTION = "file"; - private static final String HEADER_OPTION = "header"; - private static final String HELP_OPTION = "help"; - private static final String MAX_STREAMS_PER_CONNECTION = "max-streams-per-connection"; - private static final String PRIVATE_KEY_OPTION = "private-key"; - private static final String ROUTE_OPTION = "route"; - private static final String TIMEOUT_OPTION = "timeout"; - private static final String TRACE_OPTION = "trace"; - private static final String VERBOSE_OPTION = "verbose"; - private static final String SHOW_ERRORS_OPTION = "show-errors"; - private static final String SHOW_ALL_OPTION = "show-all"; - private static final String SILENT_OPTION = "silent"; - private static final String VERSION_OPTION = "version"; - private static final String STDIN_OPTION = "stdin"; - - private final CommandLine arguments; - - private CliArguments(CommandLine arguments) throws CliArgumentsException { - validateArgumentCombination(arguments); - this.arguments = arguments; - } - - static CliArguments fromRawArgs(String[] rawArgs) throws CliArgumentsException { - CommandLineParser parser = new DefaultParser(); - try { - return new CliArguments(parser.parse(optionsDefinition, rawArgs)); - } catch (ParseException e) { - throw new CliArgumentsException(e); - } - } - - private static void validateArgumentCombination(CommandLine args) throws CliArgumentsException { - if (!args.hasOption(HELP_OPTION) && !args.hasOption(VERSION_OPTION)) { - if (!args.hasOption(ENDPOINT_OPTION)) { - throw new CliArgumentsException("Endpoint must be specified"); - } - if (args.hasOption(FILE_OPTION) == args.hasOption(STDIN_OPTION)) { - throw new CliArgumentsException(String.format("Either option '%s' or '%s' must be specified", FILE_OPTION, STDIN_OPTION)); - } - if (args.hasOption(CERTIFICATE_OPTION) != args.hasOption(PRIVATE_KEY_OPTION)) { - throw new CliArgumentsException( - String.format("Both '%s' and '%s' must be specified together", CERTIFICATE_OPTION, PRIVATE_KEY_OPTION)); - } - } else if (args.hasOption(HELP_OPTION) && args.hasOption(VERSION_OPTION)) { - throw new CliArgumentsException(String.format("Cannot specify both '%s' and '%s'", HELP_OPTION, VERSION_OPTION)); - } - } - - URI endpoint() throws CliArgumentsException { - try { - return ((URL) arguments.getParsedOptionValue(ENDPOINT_OPTION)).toURI(); - } catch (ParseException | URISyntaxException e) { - throw new CliArgumentsException("Invalid endpoint: " + e.getMessage(), e); - } - } - - boolean helpSpecified() { return has(HELP_OPTION); } - - boolean versionSpecified() { return has(VERSION_OPTION); } - - OptionalInt connections() throws CliArgumentsException { return intValue(CONNECTIONS_OPTION); } - - OptionalInt maxStreamsPerConnection() throws CliArgumentsException { return intValue(MAX_STREAMS_PER_CONNECTION); } - - Optional certificateAndKey() throws CliArgumentsException { - Path certificateFile = fileValue(CERTIFICATE_OPTION).orElse(null); - Path privateKeyFile = fileValue(PRIVATE_KEY_OPTION).orElse(null); - if (privateKeyFile == null && certificateFile == null) return Optional.empty(); - return Optional.of(new CertificateAndKey(certificateFile, privateKeyFile)); - } - - Optional caCertificates() throws CliArgumentsException { return fileValue(CA_CERTIFICATES_OPTION); } - - Optional inputFile() throws CliArgumentsException { - return fileValue(FILE_OPTION); - } - - Map headers() throws CliArgumentsException { - String[] rawArguments = arguments.getOptionValues(HEADER_OPTION); - if (rawArguments == null) return Collections.emptyMap(); - Map headers = new HashMap<>(); - for (String rawArgument : rawArguments) { - if (rawArgument.startsWith("\"") || rawArgument.startsWith("'")) { - rawArgument = rawArgument.substring(1); - } - if (rawArgument.endsWith("\"") || rawArgument.endsWith("'")) { - rawArgument = rawArgument.substring(0, rawArgument.length() - 1); - } - int colonIndex = rawArgument.indexOf(':'); - if (colonIndex == -1) throw new CliArgumentsException("Invalid header: '" + rawArgument + "'"); - headers.put(rawArgument.substring(0, colonIndex), rawArgument.substring(colonIndex + 1).trim()); - } - return Collections.unmodifiableMap(headers); - } - - boolean sslHostnameVerificationDisabled() { return has(DISABLE_SSL_HOSTNAME_VERIFICATION_OPTION); } - - boolean benchmarkModeEnabled() { return has(BENCHMARK_OPTION); } - - boolean showProgress() { return ! has(SILENT_OPTION); } - - boolean showErrors() { return has(SHOW_ERRORS_OPTION) || has(SHOW_ALL_OPTION); } - - boolean showSuccesses() { return has(SHOW_ALL_OPTION); } - - Optional route() { return stringValue(ROUTE_OPTION); } - - OptionalInt traceLevel() throws CliArgumentsException { return intValue(TRACE_OPTION); } - - Optional timeout() throws CliArgumentsException { - OptionalDouble timeout = doubleValue(TIMEOUT_OPTION); - return timeout.isPresent() - ? Optional.of(Duration.ofMillis((long)(timeout.getAsDouble()*1000))) - : Optional.empty(); - } - - boolean verboseSpecified() { return has(VERBOSE_OPTION); } - - boolean readFeedFromStandardInput() { return has(STDIN_OPTION); } - - boolean dryrunEnabled() { return has(DRYRUN_OPTION); } - - private OptionalInt intValue(String option) throws CliArgumentsException { - try { - Number number = (Number) arguments.getParsedOptionValue(option); - return number != null ? OptionalInt.of(number.intValue()) : OptionalInt.empty(); - } catch (ParseException e) { - throw newInvalidValueException(option, e); - } - } - - private Optional fileValue(String option) throws CliArgumentsException { - try { - File certificateFile = (File) arguments.getParsedOptionValue(option); - if (certificateFile == null) return Optional.empty(); - return Optional.of(certificateFile.toPath()); - } catch (ParseException e) { - throw newInvalidValueException(option, e); - } - } - - private Optional stringValue(String option) { return Optional.ofNullable(arguments.getOptionValue(option)); } - - private OptionalDouble doubleValue(String option) throws CliArgumentsException { - try { - Number number = (Number) arguments.getParsedOptionValue(option); - return number != null ? OptionalDouble.of(number.doubleValue()) : OptionalDouble.empty(); - } catch (ParseException e) { - throw newInvalidValueException(option, e); - } - } - - private boolean has(String option) { return arguments.hasOption(option); } - - private static CliArgumentsException newInvalidValueException(String option, ParseException cause) { - return new CliArgumentsException(String.format("Invalid value for '%s': %s", option, cause.getMessage()), cause); - } - - private static Options createOptions() { - return new Options() - .addOption(Option.builder() - .longOpt(HELP_OPTION) - .build()) - .addOption(Option.builder() - .longOpt(VERSION_OPTION) - .build()) - .addOption(Option.builder() - .longOpt(ENDPOINT_OPTION) - .desc("URI to feed endpoint") - .hasArg() - .type(URL.class) - .build()) - .addOption(Option.builder() - .longOpt(HEADER_OPTION) - .desc("HTTP header on the form 'Name: value'") - .hasArgs() - .build()) - .addOption(Option.builder() - .longOpt(FILE_OPTION) - .type(File.class) - .desc("Path to feed file in JSON format") - .hasArg() - .build()) - .addOption(Option.builder() - .longOpt(CONNECTIONS_OPTION) - .desc("Number of concurrent HTTP/2 connections") - .hasArg() - .type(Number.class) - .build()) - .addOption(Option.builder() - .longOpt(MAX_STREAMS_PER_CONNECTION) - .desc("Maximum number of concurrent streams per HTTP/2 connection") - .hasArg() - .type(Number.class) - .build()) - .addOption(Option.builder() - .longOpt(CERTIFICATE_OPTION) - .desc("Path to PEM encoded X.509 certificate file") - .type(File.class) - .hasArg() - .build()) - .addOption(Option.builder() - .longOpt(PRIVATE_KEY_OPTION) - .desc("Path to PEM/PKCS#8 encoded private key file") - .type(File.class) - .hasArg() - .build()) - .addOption(Option.builder() - .longOpt(CA_CERTIFICATES_OPTION) - .desc("Path to file containing CA X.509 certificates encoded as PEM") - .type(File.class) - .hasArg() - .build()) - .addOption(Option.builder() - .longOpt(DISABLE_SSL_HOSTNAME_VERIFICATION_OPTION) - .desc("Disable SSL hostname verification") - .build()) - .addOption(Option.builder() - .longOpt(BENCHMARK_OPTION) - .desc("Enable benchmark mode") - .build()) - .addOption(Option.builder() - .longOpt(ROUTE_OPTION) - .desc("Target Vespa route for feed operations") - .hasArg() - .build()) - .addOption(Option.builder() - .longOpt(TIMEOUT_OPTION) - .desc("Feed operation timeout (in seconds)") - .hasArg() - .type(Number.class) - .build()) - .addOption(Option.builder() - .longOpt(TRACE_OPTION) - .desc("The trace level of network traffic. Disabled by default (=0)") - .hasArg() - .type(Number.class) - .build()) - .addOption(Option.builder() - .longOpt(STDIN_OPTION) - .desc("Read JSON input from standard input") - .build()) - .addOption(Option.builder() - .longOpt(DRYRUN_OPTION) - .desc("Enable dryrun mode where each operation succeeds after " + DryrunCluster.DELAY.toMillis() + "ms") - .build()) - .addOption(Option.builder() - .longOpt(VERBOSE_OPTION) - .desc("Print stack traces on errors") - .build()) - .addOption(Option.builder() - .longOpt(SILENT_OPTION) - .desc("Disable periodic status printing") - .build()) - .addOption(Option.builder() - .longOpt(SHOW_ERRORS_OPTION) - .desc("Print every feed operation failure") - .build()) - .addOption(Option.builder() - .longOpt(SHOW_ALL_OPTION) - .desc("Print the result of every feed operation") - .build()); - } - - void printHelp(OutputStream out) { - HelpFormatter formatter = new HelpFormatter(); - PrintWriter writer = new PrintWriter(out); - formatter.printHelp( - writer, - formatter.getWidth(), - "vespa-feed-client ", - "Vespa feed client", - optionsDefinition, - formatter.getLeftPadding(), - formatter.getDescPadding(), - ""); - writer.flush(); - } - - static class CliArgumentsException extends Exception { - CliArgumentsException(String message, Throwable cause) { super(message, cause); } - CliArgumentsException(Throwable cause) { super(cause.getMessage(), cause); } - CliArgumentsException(String message) { super(message); } - } - - static class CertificateAndKey { - final Path certificateFile; - final Path privateKeyFile; - - CertificateAndKey(Path certificateFile, Path privateKeyFile) { - this.certificateFile = certificateFile; - this.privateKeyFile = privateKeyFile; - } - } - -} diff --git a/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/CliClient.java b/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/CliClient.java deleted file mode 100644 index e40b543f26a..00000000000 --- a/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/CliClient.java +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package ai.vespa.feed.client; - -import ai.vespa.feed.client.JsonFeeder.ResultCallback; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSession; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintStream; -import java.nio.file.Files; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Main method for CLI interface - * - * @author bjorncs - */ -public class CliClient { - - private static final JsonFactory factory = new JsonFactory().disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); - - private final PrintStream systemOut; - private final PrintStream systemError; - private final InputStream systemIn; - private final Object printMonitor = new Object(); - - private CliClient(PrintStream systemOut, PrintStream systemError, InputStream systemIn) { - this.systemOut = systemOut; - this.systemError = systemError; - this.systemIn = systemIn; - } - - public static void main(String[] args) { - CliClient client = new CliClient(System.out, System.err, System.in); - int exitCode = client.run(args); - System.exit(exitCode); - } - - private int run(String[] rawArgs) { - boolean verbose = false; - try { - CliArguments cliArgs = CliArguments.fromRawArgs(rawArgs); - verbose = cliArgs.verboseSpecified(); - if (cliArgs.helpSpecified()) { - cliArgs.printHelp(systemOut); - return 0; - } - if (cliArgs.versionSpecified()) { - systemOut.println(Vespa.VERSION); - return 0; - } - try (InputStream in = createFeedInputStream(cliArgs); - FeedClient feedClient = createFeedClient(cliArgs); - JsonFeeder feeder = createJsonFeeder(feedClient, cliArgs)) { - CountDownLatch latch = new CountDownLatch(1); - AtomicReference fatal = new AtomicReference<>(); - AtomicLong successes = new AtomicLong(); - AtomicLong failures = new AtomicLong(); - long startNanos = System.nanoTime(); - if (cliArgs.showProgress()) { - Thread progressPrinter = new Thread(() -> { - try { - while ( ! latch.await(10, TimeUnit.SECONDS)) { - synchronized (printMonitor) { - printBenchmarkResult(System.nanoTime() - startNanos, successes.get(), failures.get(), feedClient.stats(), systemError); - } - } - } - catch (InterruptedException | IOException ignored) { } // doesn't happen - }, "progress-printer"); - progressPrinter.setDaemon(true); - progressPrinter.start(); - } - - feeder.feedMany(in, new ResultCallback() { - @Override public void onNextResult(Result result, FeedException error) { handleResult(result, error, successes, failures, cliArgs); } - @Override public void onError(FeedException error) { fatal.set(error); latch.countDown(); } - @Override public void onComplete() { latch.countDown(); } - }); - latch.await(); - - if (cliArgs.benchmarkModeEnabled()) { - printBenchmarkResult(System.nanoTime() - startNanos, successes.get(), failures.get(), feedClient.stats(), systemOut); - } - if (fatal.get() != null) throw fatal.get(); - } - return 0; - } catch (CliArguments.CliArgumentsException | IOException | FeedException e) { - return handleException(verbose, e); - } catch (Exception e) { - return handleException(verbose, "Unknown failure: " + e.getMessage(), e); - } - } - - private void handleResult(Result result, FeedException error, AtomicLong successes, AtomicLong failures, CliArguments args) { - if (error != null) { - failures.incrementAndGet(); - if (args.showErrors()) synchronized (printMonitor) { - systemError.println(error.getMessage()); - if (error instanceof ResultException) ((ResultException) error).getTrace().ifPresent(systemError::println); - if (args.verboseSpecified()) error.printStackTrace(systemError); - } - } - else { - successes.incrementAndGet(); - if (args.showSuccesses()) synchronized (printMonitor) { - systemError.println(result.documentId() + ": " + result.type()); - result.traceMessage().ifPresent(systemError::println); - result.resultMessage().ifPresent(systemError::println); - } - } - } - - private static FeedClient createFeedClient(CliArguments cliArgs) throws CliArguments.CliArgumentsException { - FeedClientBuilder builder = FeedClientBuilder.create(cliArgs.endpoint()); - cliArgs.connections().ifPresent(builder::setConnectionsPerEndpoint); - cliArgs.maxStreamsPerConnection().ifPresent(builder::setMaxStreamPerConnection); - if (cliArgs.sslHostnameVerificationDisabled()) { - builder.setHostnameVerifier(AcceptAllHostnameVerifier.INSTANCE); - } - cliArgs.certificateAndKey().ifPresent(c -> builder.setCertificate(c.certificateFile, c.privateKeyFile)); - cliArgs.caCertificates().ifPresent(builder::setCaCertificatesFile); - cliArgs.headers().forEach(builder::addRequestHeader); - builder.setDryrun(cliArgs.dryrunEnabled()); - return builder.build(); - } - - private static JsonFeeder createJsonFeeder(FeedClient feedClient, CliArguments cliArgs) throws CliArguments.CliArgumentsException, IOException { - JsonFeeder.Builder builder = JsonFeeder.builder(feedClient); - cliArgs.timeout().ifPresent(builder::withTimeout); - cliArgs.route().ifPresent(builder::withRoute); - cliArgs.traceLevel().ifPresent(builder::withTracelevel); - return builder.build(); - } - - private InputStream createFeedInputStream(CliArguments cliArgs) throws CliArguments.CliArgumentsException, IOException { - return cliArgs.readFeedFromStandardInput() ? systemIn : Files.newInputStream(cliArgs.inputFile().get()); - } - - private int handleException(boolean verbose, Exception e) { return handleException(verbose, e.getMessage(), e); } - - private int handleException(boolean verbose, String message, Exception exception) { - systemError.println(message); - if (verbose) { - exception.printStackTrace(systemError); - } - return 1; - } - - private static class AcceptAllHostnameVerifier implements HostnameVerifier { - static final AcceptAllHostnameVerifier INSTANCE = new AcceptAllHostnameVerifier(); - @Override public boolean verify(String hostname, SSLSession session) { return true; } - } - - static void printBenchmarkResult(long durationNanos, long successes, long failures, - OperationStats stats, OutputStream systemOut) throws IOException { - try (JsonGenerator generator = factory.createGenerator(systemOut).useDefaultPrettyPrinter()) { - generator.writeStartObject(); - - writeFloatField(generator, "feeder.seconds", durationNanos * 1e-9, 3); - generator.writeNumberField("feeder.ok.count", successes); - writeFloatField(generator, "feeder.ok.rate", successes * 1e9 / Math.max(1, durationNanos), 3); - generator.writeNumberField("feeder.error.count", failures); - generator.writeNumberField("feeder.inflight.count", stats.inflight()); - - generator.writeNumberField("http.request.count", stats.requests()); - generator.writeNumberField("http.request.bytes", stats.bytesSent()); - - generator.writeNumberField("http.exception.count", stats.exceptions()); - - generator.writeNumberField("http.response.count", stats.responses()); - generator.writeNumberField("http.response.bytes", stats.bytesReceived()); - generator.writeNumberField("http.response.error.count", stats.responses() - stats.successes()); - writeFloatField(generator, "http.response.latency.millis.min", stats.minLatencyMillis(), 3); - writeFloatField(generator, "http.response.latency.millis.avg", stats.averageLatencyMillis(), 3); - writeFloatField(generator, "http.response.latency.millis.max", stats.maxLatencyMillis(), 3); - - generator.writeObjectFieldStart("http.response.code.counts"); - for (Map.Entry entry : stats.responsesByCode().entrySet()) - generator.writeNumberField(Integer.toString(entry.getKey()), entry.getValue()); - generator.writeEndObject(); - - generator.writeEndObject(); - } - } - - private static void writeFloatField(JsonGenerator generator, String name, double value, int precision) throws IOException { - generator.writeFieldName(name); - generator.writeNumber(String.format("%." + precision + "f", value)); - } - -} diff --git a/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java b/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java new file mode 100644 index 00000000000..2fc7e5af7b4 --- /dev/null +++ b/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliArguments.java @@ -0,0 +1,338 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.feed.client.impl; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +import java.io.File; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; + +/** + * Parses command line arguments + * + * @author bjorncs + */ +class CliArguments { + + private static final Options optionsDefinition = createOptions(); + + private static final String BENCHMARK_OPTION = "benchmark"; + private static final String CA_CERTIFICATES_OPTION = "ca-certificates"; + private static final String CERTIFICATE_OPTION = "certificate"; + private static final String CONNECTIONS_OPTION = "connections"; + private static final String DISABLE_SSL_HOSTNAME_VERIFICATION_OPTION = "disable-ssl-hostname-verification"; + private static final String DRYRUN_OPTION = "dryrun"; + private static final String ENDPOINT_OPTION = "endpoint"; + private static final String FILE_OPTION = "file"; + private static final String HEADER_OPTION = "header"; + private static final String HELP_OPTION = "help"; + private static final String MAX_STREAMS_PER_CONNECTION = "max-streams-per-connection"; + private static final String PRIVATE_KEY_OPTION = "private-key"; + private static final String ROUTE_OPTION = "route"; + private static final String TIMEOUT_OPTION = "timeout"; + private static final String TRACE_OPTION = "trace"; + private static final String VERBOSE_OPTION = "verbose"; + private static final String SHOW_ERRORS_OPTION = "show-errors"; + private static final String SHOW_ALL_OPTION = "show-all"; + private static final String SILENT_OPTION = "silent"; + private static final String VERSION_OPTION = "version"; + private static final String STDIN_OPTION = "stdin"; + + private final CommandLine arguments; + + private CliArguments(CommandLine arguments) throws CliArgumentsException { + validateArgumentCombination(arguments); + this.arguments = arguments; + } + + static CliArguments fromRawArgs(String[] rawArgs) throws CliArgumentsException { + CommandLineParser parser = new DefaultParser(); + try { + return new CliArguments(parser.parse(optionsDefinition, rawArgs)); + } catch (ParseException e) { + throw new CliArgumentsException(e); + } + } + + private static void validateArgumentCombination(CommandLine args) throws CliArgumentsException { + if (!args.hasOption(HELP_OPTION) && !args.hasOption(VERSION_OPTION)) { + if (!args.hasOption(ENDPOINT_OPTION)) { + throw new CliArgumentsException("Endpoint must be specified"); + } + if (args.hasOption(FILE_OPTION) == args.hasOption(STDIN_OPTION)) { + throw new CliArgumentsException(String.format("Either option '%s' or '%s' must be specified", FILE_OPTION, STDIN_OPTION)); + } + if (args.hasOption(CERTIFICATE_OPTION) != args.hasOption(PRIVATE_KEY_OPTION)) { + throw new CliArgumentsException( + String.format("Both '%s' and '%s' must be specified together", CERTIFICATE_OPTION, PRIVATE_KEY_OPTION)); + } + } else if (args.hasOption(HELP_OPTION) && args.hasOption(VERSION_OPTION)) { + throw new CliArgumentsException(String.format("Cannot specify both '%s' and '%s'", HELP_OPTION, VERSION_OPTION)); + } + } + + URI endpoint() throws CliArgumentsException { + try { + return ((URL) arguments.getParsedOptionValue(ENDPOINT_OPTION)).toURI(); + } catch (ParseException | URISyntaxException e) { + throw new CliArgumentsException("Invalid endpoint: " + e.getMessage(), e); + } + } + + boolean helpSpecified() { return has(HELP_OPTION); } + + boolean versionSpecified() { return has(VERSION_OPTION); } + + OptionalInt connections() throws CliArgumentsException { return intValue(CONNECTIONS_OPTION); } + + OptionalInt maxStreamsPerConnection() throws CliArgumentsException { return intValue(MAX_STREAMS_PER_CONNECTION); } + + Optional certificateAndKey() throws CliArgumentsException { + Path certificateFile = fileValue(CERTIFICATE_OPTION).orElse(null); + Path privateKeyFile = fileValue(PRIVATE_KEY_OPTION).orElse(null); + if (privateKeyFile == null && certificateFile == null) return Optional.empty(); + return Optional.of(new CertificateAndKey(certificateFile, privateKeyFile)); + } + + Optional caCertificates() throws CliArgumentsException { return fileValue(CA_CERTIFICATES_OPTION); } + + Optional inputFile() throws CliArgumentsException { + return fileValue(FILE_OPTION); + } + + Map headers() throws CliArgumentsException { + String[] rawArguments = arguments.getOptionValues(HEADER_OPTION); + if (rawArguments == null) return Collections.emptyMap(); + Map headers = new HashMap<>(); + for (String rawArgument : rawArguments) { + if (rawArgument.startsWith("\"") || rawArgument.startsWith("'")) { + rawArgument = rawArgument.substring(1); + } + if (rawArgument.endsWith("\"") || rawArgument.endsWith("'")) { + rawArgument = rawArgument.substring(0, rawArgument.length() - 1); + } + int colonIndex = rawArgument.indexOf(':'); + if (colonIndex == -1) throw new CliArgumentsException("Invalid header: '" + rawArgument + "'"); + headers.put(rawArgument.substring(0, colonIndex), rawArgument.substring(colonIndex + 1).trim()); + } + return Collections.unmodifiableMap(headers); + } + + boolean sslHostnameVerificationDisabled() { return has(DISABLE_SSL_HOSTNAME_VERIFICATION_OPTION); } + + boolean benchmarkModeEnabled() { return has(BENCHMARK_OPTION); } + + boolean showProgress() { return ! has(SILENT_OPTION); } + + boolean showErrors() { return has(SHOW_ERRORS_OPTION) || has(SHOW_ALL_OPTION); } + + boolean showSuccesses() { return has(SHOW_ALL_OPTION); } + + Optional route() { return stringValue(ROUTE_OPTION); } + + OptionalInt traceLevel() throws CliArgumentsException { return intValue(TRACE_OPTION); } + + Optional timeout() throws CliArgumentsException { + OptionalDouble timeout = doubleValue(TIMEOUT_OPTION); + return timeout.isPresent() + ? Optional.of(Duration.ofMillis((long)(timeout.getAsDouble()*1000))) + : Optional.empty(); + } + + boolean verboseSpecified() { return has(VERBOSE_OPTION); } + + boolean readFeedFromStandardInput() { return has(STDIN_OPTION); } + + boolean dryrunEnabled() { return has(DRYRUN_OPTION); } + + private OptionalInt intValue(String option) throws CliArgumentsException { + try { + Number number = (Number) arguments.getParsedOptionValue(option); + return number != null ? OptionalInt.of(number.intValue()) : OptionalInt.empty(); + } catch (ParseException e) { + throw newInvalidValueException(option, e); + } + } + + private Optional fileValue(String option) throws CliArgumentsException { + try { + File certificateFile = (File) arguments.getParsedOptionValue(option); + if (certificateFile == null) return Optional.empty(); + return Optional.of(certificateFile.toPath()); + } catch (ParseException e) { + throw newInvalidValueException(option, e); + } + } + + private Optional stringValue(String option) { return Optional.ofNullable(arguments.getOptionValue(option)); } + + private OptionalDouble doubleValue(String option) throws CliArgumentsException { + try { + Number number = (Number) arguments.getParsedOptionValue(option); + return number != null ? OptionalDouble.of(number.doubleValue()) : OptionalDouble.empty(); + } catch (ParseException e) { + throw newInvalidValueException(option, e); + } + } + + private boolean has(String option) { return arguments.hasOption(option); } + + private static CliArgumentsException newInvalidValueException(String option, ParseException cause) { + return new CliArgumentsException(String.format("Invalid value for '%s': %s", option, cause.getMessage()), cause); + } + + private static Options createOptions() { + return new Options() + .addOption(Option.builder() + .longOpt(HELP_OPTION) + .build()) + .addOption(Option.builder() + .longOpt(VERSION_OPTION) + .build()) + .addOption(Option.builder() + .longOpt(ENDPOINT_OPTION) + .desc("URI to feed endpoint") + .hasArg() + .type(URL.class) + .build()) + .addOption(Option.builder() + .longOpt(HEADER_OPTION) + .desc("HTTP header on the form 'Name: value'") + .hasArgs() + .build()) + .addOption(Option.builder() + .longOpt(FILE_OPTION) + .type(File.class) + .desc("Path to feed file in JSON format") + .hasArg() + .build()) + .addOption(Option.builder() + .longOpt(CONNECTIONS_OPTION) + .desc("Number of concurrent HTTP/2 connections") + .hasArg() + .type(Number.class) + .build()) + .addOption(Option.builder() + .longOpt(MAX_STREAMS_PER_CONNECTION) + .desc("Maximum number of concurrent streams per HTTP/2 connection") + .hasArg() + .type(Number.class) + .build()) + .addOption(Option.builder() + .longOpt(CERTIFICATE_OPTION) + .desc("Path to PEM encoded X.509 certificate file") + .type(File.class) + .hasArg() + .build()) + .addOption(Option.builder() + .longOpt(PRIVATE_KEY_OPTION) + .desc("Path to PEM/PKCS#8 encoded private key file") + .type(File.class) + .hasArg() + .build()) + .addOption(Option.builder() + .longOpt(CA_CERTIFICATES_OPTION) + .desc("Path to file containing CA X.509 certificates encoded as PEM") + .type(File.class) + .hasArg() + .build()) + .addOption(Option.builder() + .longOpt(DISABLE_SSL_HOSTNAME_VERIFICATION_OPTION) + .desc("Disable SSL hostname verification") + .build()) + .addOption(Option.builder() + .longOpt(BENCHMARK_OPTION) + .desc("Enable benchmark mode") + .build()) + .addOption(Option.builder() + .longOpt(ROUTE_OPTION) + .desc("Target Vespa route for feed operations") + .hasArg() + .build()) + .addOption(Option.builder() + .longOpt(TIMEOUT_OPTION) + .desc("Feed operation timeout (in seconds)") + .hasArg() + .type(Number.class) + .build()) + .addOption(Option.builder() + .longOpt(TRACE_OPTION) + .desc("The trace level of network traffic. Disabled by default (=0)") + .hasArg() + .type(Number.class) + .build()) + .addOption(Option.builder() + .longOpt(STDIN_OPTION) + .desc("Read JSON input from standard input") + .build()) + .addOption(Option.builder() + .longOpt(DRYRUN_OPTION) + .desc("Enable dryrun mode where each operation succeeds after " + DryrunCluster.DELAY.toMillis() + "ms") + .build()) + .addOption(Option.builder() + .longOpt(VERBOSE_OPTION) + .desc("Print stack traces on errors") + .build()) + .addOption(Option.builder() + .longOpt(SILENT_OPTION) + .desc("Disable periodic status printing") + .build()) + .addOption(Option.builder() + .longOpt(SHOW_ERRORS_OPTION) + .desc("Print every feed operation failure") + .build()) + .addOption(Option.builder() + .longOpt(SHOW_ALL_OPTION) + .desc("Print the result of every feed operation") + .build()); + } + + void printHelp(OutputStream out) { + HelpFormatter formatter = new HelpFormatter(); + PrintWriter writer = new PrintWriter(out); + formatter.printHelp( + writer, + formatter.getWidth(), + "vespa-feed-client ", + "Vespa feed client", + optionsDefinition, + formatter.getLeftPadding(), + formatter.getDescPadding(), + ""); + writer.flush(); + } + + static class CliArgumentsException extends Exception { + CliArgumentsException(String message, Throwable cause) { super(message, cause); } + CliArgumentsException(Throwable cause) { super(cause.getMessage(), cause); } + CliArgumentsException(String message) { super(message); } + } + + static class CertificateAndKey { + final Path certificateFile; + final Path privateKeyFile; + + CertificateAndKey(Path certificateFile, Path privateKeyFile) { + this.certificateFile = certificateFile; + this.privateKeyFile = privateKeyFile; + } + } + +} diff --git a/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliClient.java b/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliClient.java new file mode 100644 index 00000000000..7e036b8dec3 --- /dev/null +++ b/vespa-feed-client-cli/src/main/java/ai/vespa/feed/client/impl/CliClient.java @@ -0,0 +1,207 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.feed.client.impl; + +import ai.vespa.feed.client.FeedClient; +import ai.vespa.feed.client.FeedClientBuilder; +import ai.vespa.feed.client.FeedException; +import ai.vespa.feed.client.JsonFeeder; +import ai.vespa.feed.client.JsonFeeder.ResultCallback; +import ai.vespa.feed.client.OperationStats; +import ai.vespa.feed.client.Result; +import ai.vespa.feed.client.ResultException; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.file.Files; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Main method for CLI interface + * + * @author bjorncs + */ +public class CliClient { + + private static final JsonFactory factory = new JsonFactory().disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + + private final PrintStream systemOut; + private final PrintStream systemError; + private final InputStream systemIn; + private final Object printMonitor = new Object(); + + private CliClient(PrintStream systemOut, PrintStream systemError, InputStream systemIn) { + this.systemOut = systemOut; + this.systemError = systemError; + this.systemIn = systemIn; + } + + public static void main(String[] args) { + CliClient client = new CliClient(System.out, System.err, System.in); + int exitCode = client.run(args); + System.exit(exitCode); + } + + private int run(String[] rawArgs) { + boolean verbose = false; + try { + CliArguments cliArgs = CliArguments.fromRawArgs(rawArgs); + verbose = cliArgs.verboseSpecified(); + if (cliArgs.helpSpecified()) { + cliArgs.printHelp(systemOut); + return 0; + } + if (cliArgs.versionSpecified()) { + systemOut.println(Vespa.VERSION); + return 0; + } + try (InputStream in = createFeedInputStream(cliArgs); + FeedClient feedClient = createFeedClient(cliArgs); + JsonFeeder feeder = createJsonFeeder(feedClient, cliArgs)) { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference fatal = new AtomicReference<>(); + AtomicLong successes = new AtomicLong(); + AtomicLong failures = new AtomicLong(); + long startNanos = System.nanoTime(); + if (cliArgs.showProgress()) { + Thread progressPrinter = new Thread(() -> { + try { + while ( ! latch.await(10, TimeUnit.SECONDS)) { + synchronized (printMonitor) { + printBenchmarkResult(System.nanoTime() - startNanos, successes.get(), failures.get(), feedClient.stats(), systemError); + } + } + } + catch (InterruptedException | IOException ignored) { } // doesn't happen + }, "progress-printer"); + progressPrinter.setDaemon(true); + progressPrinter.start(); + } + + feeder.feedMany(in, new ResultCallback() { + @Override public void onNextResult(Result result, FeedException error) { handleResult(result, error, successes, failures, cliArgs); } + @Override public void onError(FeedException error) { fatal.set(error); latch.countDown(); } + @Override public void onComplete() { latch.countDown(); } + }); + latch.await(); + + if (cliArgs.benchmarkModeEnabled()) { + printBenchmarkResult(System.nanoTime() - startNanos, successes.get(), failures.get(), feedClient.stats(), systemOut); + } + if (fatal.get() != null) throw fatal.get(); + } + return 0; + } catch (CliArguments.CliArgumentsException | IOException | FeedException e) { + return handleException(verbose, e); + } catch (Exception e) { + return handleException(verbose, "Unknown failure: " + e.getMessage(), e); + } + } + + private void handleResult(Result result, FeedException error, AtomicLong successes, AtomicLong failures, CliArguments args) { + if (error != null) { + failures.incrementAndGet(); + if (args.showErrors()) synchronized (printMonitor) { + systemError.println(error.getMessage()); + if (error instanceof ResultException) ((ResultException) error).getTrace().ifPresent(systemError::println); + if (args.verboseSpecified()) error.printStackTrace(systemError); + } + } + else { + successes.incrementAndGet(); + if (args.showSuccesses()) synchronized (printMonitor) { + systemError.println(result.documentId() + ": " + result.type()); + result.traceMessage().ifPresent(systemError::println); + result.resultMessage().ifPresent(systemError::println); + } + } + } + + private static FeedClient createFeedClient(CliArguments cliArgs) throws CliArguments.CliArgumentsException { + FeedClientBuilder builder = FeedClientBuilder.create(cliArgs.endpoint()); + cliArgs.connections().ifPresent(builder::setConnectionsPerEndpoint); + cliArgs.maxStreamsPerConnection().ifPresent(builder::setMaxStreamPerConnection); + if (cliArgs.sslHostnameVerificationDisabled()) { + builder.setHostnameVerifier(AcceptAllHostnameVerifier.INSTANCE); + } + cliArgs.certificateAndKey().ifPresent(c -> builder.setCertificate(c.certificateFile, c.privateKeyFile)); + cliArgs.caCertificates().ifPresent(builder::setCaCertificatesFile); + cliArgs.headers().forEach(builder::addRequestHeader); + builder.setDryrun(cliArgs.dryrunEnabled()); + return builder.build(); + } + + private static JsonFeeder createJsonFeeder(FeedClient feedClient, CliArguments cliArgs) throws CliArguments.CliArgumentsException, IOException { + JsonFeeder.Builder builder = JsonFeeder.builder(feedClient); + cliArgs.timeout().ifPresent(builder::withTimeout); + cliArgs.route().ifPresent(builder::withRoute); + cliArgs.traceLevel().ifPresent(builder::withTracelevel); + return builder.build(); + } + + private InputStream createFeedInputStream(CliArguments cliArgs) throws CliArguments.CliArgumentsException, IOException { + return cliArgs.readFeedFromStandardInput() ? systemIn : Files.newInputStream(cliArgs.inputFile().get()); + } + + private int handleException(boolean verbose, Exception e) { return handleException(verbose, e.getMessage(), e); } + + private int handleException(boolean verbose, String message, Exception exception) { + systemError.println(message); + if (verbose) { + exception.printStackTrace(systemError); + } + return 1; + } + + private static class AcceptAllHostnameVerifier implements HostnameVerifier { + static final AcceptAllHostnameVerifier INSTANCE = new AcceptAllHostnameVerifier(); + @Override public boolean verify(String hostname, SSLSession session) { return true; } + } + + static void printBenchmarkResult(long durationNanos, long successes, long failures, + OperationStats stats, OutputStream systemOut) throws IOException { + try (JsonGenerator generator = factory.createGenerator(systemOut).useDefaultPrettyPrinter()) { + generator.writeStartObject(); + + writeFloatField(generator, "feeder.seconds", durationNanos * 1e-9, 3); + generator.writeNumberField("feeder.ok.count", successes); + writeFloatField(generator, "feeder.ok.rate", successes * 1e9 / Math.max(1, durationNanos), 3); + generator.writeNumberField("feeder.error.count", failures); + generator.writeNumberField("feeder.inflight.count", stats.inflight()); + + generator.writeNumberField("http.request.count", stats.requests()); + generator.writeNumberField("http.request.bytes", stats.bytesSent()); + + generator.writeNumberField("http.exception.count", stats.exceptions()); + + generator.writeNumberField("http.response.count", stats.responses()); + generator.writeNumberField("http.response.bytes", stats.bytesReceived()); + generator.writeNumberField("http.response.error.count", stats.responses() - stats.successes()); + writeFloatField(generator, "http.response.latency.millis.min", stats.minLatencyMillis(), 3); + writeFloatField(generator, "http.response.latency.millis.avg", stats.averageLatencyMillis(), 3); + writeFloatField(generator, "http.response.latency.millis.max", stats.maxLatencyMillis(), 3); + + generator.writeObjectFieldStart("http.response.code.counts"); + for (Map.Entry entry : stats.responsesByCode().entrySet()) + generator.writeNumberField(Integer.toString(entry.getKey()), entry.getValue()); + generator.writeEndObject(); + + generator.writeEndObject(); + } + } + + private static void writeFloatField(JsonGenerator generator, String name, double value, int precision) throws IOException { + generator.writeFieldName(name); + generator.writeNumber(String.format("%." + precision + "f", value)); + } + +} diff --git a/vespa-feed-client-cli/src/main/sh/vespa-feed-client-standalone.sh b/vespa-feed-client-cli/src/main/sh/vespa-feed-client-standalone.sh index b236a516691..c4e70c362b0 100755 --- a/vespa-feed-client-cli/src/main/sh/vespa-feed-client-standalone.sh +++ b/vespa-feed-client-cli/src/main/sh/vespa-feed-client-standalone.sh @@ -6,4 +6,4 @@ exec java \ -Xms128m -Xmx2048m \ --add-opens=java.base/sun.security.ssl=ALL-UNNAMED \ -Djava.util.logging.config.file=`dirname $0`/logging.properties \ --cp `dirname $0`/vespa-feed-client-cli-jar-with-dependencies.jar ai.vespa.feed.client.CliClient "$@" +-cp `dirname $0`/vespa-feed-client-cli-jar-with-dependencies.jar ai.vespa.feed.client.impl.CliClient "$@" diff --git a/vespa-feed-client-cli/src/main/sh/vespa-feed-client.sh b/vespa-feed-client-cli/src/main/sh/vespa-feed-client.sh index fbd172e7423..7dbdc056524 100755 --- a/vespa-feed-client-cli/src/main/sh/vespa-feed-client.sh +++ b/vespa-feed-client-cli/src/main/sh/vespa-feed-client.sh @@ -81,4 +81,4 @@ exec java \ -Xms128m -Xmx2048m $(getJavaOptionsIPV46) \ --add-opens=java.base/sun.security.ssl=ALL-UNNAMED \ -Djava.util.logging.config.file=${VESPA_HOME}/conf/vespa-feed-client/logging.properties \ --cp ${VESPA_HOME}/lib/jars/vespa-feed-client-cli-jar-with-dependencies.jar ai.vespa.feed.client.CliClient "$@" +-cp ${VESPA_HOME}/lib/jars/vespa-feed-client-cli-jar-with-dependencies.jar ai.vespa.feed.client.impl.CliClient "$@" diff --git a/vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/CliArgumentsTest.java b/vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/CliArgumentsTest.java deleted file mode 100644 index 622956db530..00000000000 --- a/vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/CliArgumentsTest.java +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package ai.vespa.feed.client; - -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.time.Duration; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * @author bjorncs - */ -class CliArgumentsTest { - - @Test - void parses_parameters_correctly() throws CliArguments.CliArgumentsException { - CliArguments args = CliArguments.fromRawArgs(new String[]{ - "--endpoint=https://vespa.ai:4443/", "--file=feed.json", "--connections=10", - "--max-streams-per-connection=128", "--certificate=cert.pem", "--private-key=key.pem", - "--ca-certificates=ca-certs.pem", "--disable-ssl-hostname-verification", - "--header=\"My-Header: my-value\"", "--header", "Another-Header: another-value", "--benchmark", - "--route=myroute", "--timeout=0.125", "--trace=9", "--verbose", "--silent", "--show-errors", "--show-all"}); - assertEquals(URI.create("https://vespa.ai:4443/"), args.endpoint()); - assertEquals(Paths.get("feed.json"), args.inputFile().get()); - assertEquals(10, args.connections().getAsInt()); - assertEquals(128, args.maxStreamsPerConnection().getAsInt()); - assertEquals(Paths.get("cert.pem"), args.certificateAndKey().get().certificateFile); - assertEquals(Paths.get("key.pem"), args.certificateAndKey().get().privateKeyFile); - assertEquals(Paths.get("ca-certs.pem"), args.caCertificates().get()); - assertTrue(args.sslHostnameVerificationDisabled()); - assertFalse(args.helpSpecified()); - assertFalse(args.versionSpecified()); - assertEquals(2, args.headers().size()); - assertEquals("my-value", args.headers().get("My-Header")); - assertEquals("another-value", args.headers().get("Another-Header")); - assertTrue(args.benchmarkModeEnabled()); - assertEquals("myroute", args.route().get()); - assertEquals(Duration.ofMillis(125), args.timeout().get()); - assertEquals(9, args.traceLevel().getAsInt()); - assertTrue(args.verboseSpecified()); - assertTrue(args.showErrors()); - assertTrue(args.showSuccesses()); - assertFalse(args.showProgress()); - } - - @Test - void fails_on_missing_parameters() { - CliArguments.CliArgumentsException exception = assertThrows( - CliArguments.CliArgumentsException.class, - () -> CliArguments.fromRawArgs(new String[] {"--file=/path/to/file", "--stdin"})); - assertEquals("Endpoint must be specified", exception.getMessage()); - } - - @Test - void fails_on_conflicting_parameters() { - CliArguments.CliArgumentsException exception = assertThrows( - CliArguments.CliArgumentsException.class, - () -> CliArguments.fromRawArgs(new String[] {"--endpoint=https://endpoint", "--file=/path/to/file", "--stdin"})); - assertEquals("Either option 'file' or 'stdin' must be specified", exception.getMessage()); - - exception = assertThrows( - CliArguments.CliArgumentsException.class, - () -> CliArguments.fromRawArgs(new String[] {"--endpoint=https://endpoint"})); - assertEquals("Either option 'file' or 'stdin' must be specified", exception.getMessage()); - } - - @Test - void generated_help_page_contains_expected_description() throws CliArguments.CliArgumentsException, IOException { - CliArguments args = CliArguments.fromRawArgs(new String[]{"--help"}); - assertTrue(args.helpSpecified()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - args.printHelp(out); - String text = out.toString(); - String expectedHelp = new String(Files.readAllBytes(Paths.get("src", "test", "resources", "help.txt"))); - assertEquals(expectedHelp, text); - } - -} diff --git a/vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliArgumentsTest.java b/vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliArgumentsTest.java new file mode 100644 index 00000000000..19b93c3172b --- /dev/null +++ b/vespa-feed-client-cli/src/test/java/ai/vespa/feed/client/impl/CliArgumentsTest.java @@ -0,0 +1,85 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package ai.vespa.feed.client.impl; + +import ai.vespa.feed.client.impl.CliArguments; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author bjorncs + */ +class CliArgumentsTest { + + @Test + void parses_parameters_correctly() throws CliArguments.CliArgumentsException { + CliArguments args = CliArguments.fromRawArgs(new String[]{ + "--endpoint=https://vespa.ai:4443/", "--file=feed.json", "--connections=10", + "--max-streams-per-connection=128", "--certificate=cert.pem", "--private-key=key.pem", + "--ca-certificates=ca-certs.pem", "--disable-ssl-hostname-verification", + "--header=\"My-Header: my-value\"", "--header", "Another-Header: another-value", "--benchmark", + "--route=myroute", "--timeout=0.125", "--trace=9", "--verbose", "--silent", "--show-errors", "--show-all"}); + assertEquals(URI.create("https://vespa.ai:4443/"), args.endpoint()); + assertEquals(Paths.get("feed.json"), args.inputFile().get()); + assertEquals(10, args.connections().getAsInt()); + assertEquals(128, args.maxStreamsPerConnection().getAsInt()); + assertEquals(Paths.get("cert.pem"), args.certificateAndKey().get().certificateFile); + assertEquals(Paths.get("key.pem"), args.certificateAndKey().get().privateKeyFile); + assertEquals(Paths.get("ca-certs.pem"), args.caCertificates().get()); + assertTrue(args.sslHostnameVerificationDisabled()); + assertFalse(args.helpSpecified()); + assertFalse(args.versionSpecified()); + assertEquals(2, args.headers().size()); + assertEquals("my-value", args.headers().get("My-Header")); + assertEquals("another-value", args.headers().get("Another-Header")); + assertTrue(args.benchmarkModeEnabled()); + assertEquals("myroute", args.route().get()); + assertEquals(Duration.ofMillis(125), args.timeout().get()); + assertEquals(9, args.traceLevel().getAsInt()); + assertTrue(args.verboseSpecified()); + assertTrue(args.showErrors()); + assertTrue(args.showSuccesses()); + assertFalse(args.showProgress()); + } + + @Test + void fails_on_missing_parameters() { + CliArguments.CliArgumentsException exception = assertThrows( + CliArguments.CliArgumentsException.class, + () -> CliArguments.fromRawArgs(new String[] {"--file=/path/to/file", "--stdin"})); + assertEquals("Endpoint must be specified", exception.getMessage()); + } + + @Test + void fails_on_conflicting_parameters() { + CliArguments.CliArgumentsException exception = assertThrows( + CliArguments.CliArgumentsException.class, + () -> CliArguments.fromRawArgs(new String[] {"--endpoint=https://endpoint", "--file=/path/to/file", "--stdin"})); + assertEquals("Either option 'file' or 'stdin' must be specified", exception.getMessage()); + + exception = assertThrows( + CliArguments.CliArgumentsException.class, + () -> CliArguments.fromRawArgs(new String[] {"--endpoint=https://endpoint"})); + assertEquals("Either option 'file' or 'stdin' must be specified", exception.getMessage()); + } + + @Test + void generated_help_page_contains_expected_description() throws CliArguments.CliArgumentsException, IOException { + CliArguments args = CliArguments.fromRawArgs(new String[]{"--help"}); + assertTrue(args.helpSpecified()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + args.printHelp(out); + String text = out.toString(); + String expectedHelp = new String(Files.readAllBytes(Paths.get("src", "test", "resources", "help.txt"))); + assertEquals(expectedHelp, text); + } + +} -- cgit v1.2.3