SQL: Replace the cli fixture with in-process testing (elastic/x-pack-elasticsearch#3889)

I'm really really sad to be removing the cli-fixture but I've had
trouble with it leaking recently it is pretty slow. Beyond that, we'd
prefer that our test fixture only fixture things that are external
depndencies.

So, yeah, I'm removing it. So we get faster tests and no chance of
leaking processes. We lose some "realness" in the tests. Instead of
interacting with the CLI like a real user we embed it in the test
process. That means we don't test the forking, we don't test the
executable jar, and we don't test the jLine console detection stuff. On
the other hand we were kind of forcing the jLine console detection stuff
in a funky way with the fixture anyway. And we test the executable jar
in the packaging tests. And that'll have to do.

I haven't renamed `RemoteCli` because it'd bloat this commit with
mechanical changes that'd make it hard to review. I'll rename it in a
followup commit.

This also updates jLine so we can disable blinking to matching
parentheses during testing. I have no clue why, but this wasn't
happening when we used the fixture. The trouble with the blinking is
that it is based on *time* so it slows things down. Worse, it works
inconsistently! Sometimes it spits out sensible ascii codes and
sometimes it, well, spits out weird garbage. When you use it in person
it works fine though. So we keep it on when not testing.

Cleans up some redundancy in when testing CLI errors. Less copy and
paste good.

I was tempted to disable the xterm emulation entirely while working on
this because upgrading jLine changed a few things and it was a real pain
to update. But If we turned that off then we'd have *nothing* testing
the colors and such. That'd be a shame because we use color in the
output to commicate stuff. I like it so I don't want to break it.

While I was there, I replaces the cli connector's `PrintWriter` with a
`BufferedWriter`. The `PrintWriter` was kind of a trap because `println`
would fail to work properly on windows because we force the terminal
into xterm mode and it doesn't know what to do with windows line
endings. Windows.....

Additionally I fixed a race condition between disabling echo when
reading passwords and fast writers. We were disabling the echo shortly
after sending the prompt. A fast enough writer could send us text before
the echo disable kicked in. Now I delegate to `LineReader#readLine`
with a special echo mask that disables echo. This is both easier to test
and doesn't seem to have the race condition. This race condition was
failing the tests because they are so much faster now. Yay!

Original commit: elastic/x-pack-elasticsearch@d0ec027396
This commit is contained in:
Nik Everett 2018-02-27 09:24:16 -08:00 committed by GitHub
parent 065f9aa834
commit d962d33a2a
16 changed files with 454 additions and 416 deletions

View File

@ -13,7 +13,7 @@ apply plugin: 'elasticsearch.build'
description = 'Command line interface to Elasticsearch that speaks SQL'
dependencies {
compile "org.jline:jline:3.3.1"
compile "org.jline:jline:3.6.0"
compile xpackProject('plugin:sql:sql-shared-client')
compile xpackProject('plugin:sql:sql-proto')
compile "org.elasticsearch:elasticsearch-cli:${version}"
@ -38,19 +38,38 @@ dependencyLicenses {
ignoreSha 'sql-shared-client'
}
/*
* Bundle all dependencies into the main jar and mark it as executable it
* can be easilly shipped around and used.
*/
jar {
// Bundle all dependencies into the jar.
from {
configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
configurations.runtime.collect { it.isDirectory() ? it : zipTree(it) }
}
// Make the jar "executable" with `java -jar`
manifest {
attributes 'Main-Class': 'org.elasticsearch.xpack.sql.cli.Cli'
}
}
/*
* Build a jar that doesn't include the dependencies bundled that we can
* include with QA tests along side Elasticsearch without breaking
* jarhell.
*/
configurations {
nodeps
}
task nodepsJar(type: Jar) {
appendix 'nodeps'
from sourceSets.main.output
}
artifacts {
nodeps nodepsJar
}
thirdPartyAudit.excludes = [
// jLine's optional dependencies
'org.apache.sshd.client.SshClient',
'org.apache.sshd.client.auth.keyboard.UserInteraction',
'org.apache.sshd.client.channel.ChannelShell',
@ -75,7 +94,9 @@ thirdPartyAudit.excludes = [
'org.apache.sshd.server.scp.ScpCommandFactory$Builder',
'org.apache.sshd.server.session.ServerSession',
'org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory$Builder',
'org.mozilla.universalchardet.UniversalDetector'
'org.mozilla.universalchardet.UniversalDetector',
'org.fusesource.jansi.internal.Kernel32$FOCUS_EVENT_RECORD',
'org.fusesource.jansi.internal.Kernel32$MOUSE_EVENT_RECORD',
]
task runcli {

View File

@ -1 +0,0 @@
d8a30137fe4ee2246b71b3915baac767d348c5bb

View File

@ -0,0 +1 @@
c8ecc302d6b7d19da41c66be7d428c17cd6b12b2

View File

@ -24,7 +24,7 @@ import org.elasticsearch.xpack.sql.client.HttpClient;
import org.elasticsearch.xpack.sql.client.shared.ClientException;
import org.elasticsearch.xpack.sql.client.shared.ConnectionConfiguration;
import org.elasticsearch.xpack.sql.client.shared.Version;
import org.jline.terminal.TerminalBuilder;
import java.io.IOException;
import java.net.ConnectException;
import java.util.Arrays;
@ -36,22 +36,6 @@ public class Cli extends LoggingAwareCommand {
private final OptionSpec<Boolean> checkOption;
private final OptionSpec<String> connectionString;
private Cli() {
super("Elasticsearch SQL CLI");
parser.acceptsAll(Arrays.asList("d", "debug"), "Enable debug logging");
this.keystoreLocation = parser.acceptsAll(
Arrays.asList("k", "keystore_location"),
"Location of a keystore to use when setting up SSL. "
+ "If specified then the CLI will prompt for a keystore password. "
+ "If specified when the uri isn't https then an error is thrown.")
.withRequiredArg().ofType(String.class);
this.checkOption = parser.acceptsAll(Arrays.asList("c", "check"),
"Enable initial connection check on startup")
.withRequiredArg().ofType(Boolean.class)
.defaultsTo(Boolean.parseBoolean(System.getProperty("cli.check", "true")));
this.connectionString = parser.nonOptions("uri");
}
/**
* Use this VM Options to run in IntelliJ or Eclipse:
* -Dorg.jline.terminal.type=xterm-256color
@ -61,7 +45,7 @@ public class Cli extends LoggingAwareCommand {
* -Dorg.jline.terminal.dumb=true
*/
public static void main(String[] args) throws Exception {
final Cli cli = new Cli();
final Cli cli = new Cli(new JLineTerminal(TerminalBuilder.builder().build(), true));
configureJLineLogging();
int status = cli.main(args, Terminal.DEFAULT);
if (status != ExitCodes.OK) {
@ -79,6 +63,28 @@ public class Cli extends LoggingAwareCommand {
}
}
private final CliTerminal cliTerminal;
/**
* Build the CLI.
*/
public Cli(CliTerminal cliTerminal) {
super("Elasticsearch SQL CLI");
this.cliTerminal = cliTerminal;
parser.acceptsAll(Arrays.asList("d", "debug"), "Enable debug logging");
this.keystoreLocation = parser.acceptsAll(
Arrays.asList("k", "keystore_location"),
"Location of a keystore to use when setting up SSL. "
+ "If specified then the CLI will prompt for a keystore password. "
+ "If specified when the uri isn't https then an error is thrown.")
.withRequiredArg().ofType(String.class);
this.checkOption = parser.acceptsAll(Arrays.asList("c", "check"),
"Enable initial connection check on startup")
.withRequiredArg().ofType(Boolean.class)
.defaultsTo(Boolean.parseBoolean(System.getProperty("cli.check", "true")));
this.connectionString = parser.nonOptions("uri");
}
@Override
protected void execute(org.elasticsearch.cli.Terminal terminal, OptionSet options) throws Exception {
boolean debug = options.has("d") || options.has("debug");
@ -105,7 +111,7 @@ public class Cli extends LoggingAwareCommand {
new ServerInfoCliCommand(),
new ServerQueryCliCommand()
);
try (CliTerminal cliTerminal = new JLineTerminal()) {
try {
ConnectionBuilder connectionBuilder = new ConnectionBuilder(cliTerminal);
ConnectionConfiguration con = connectionBuilder.buildConnection(uri, keystoreLocation);
CliSession cliSession = new CliSession(new HttpClient(con));
@ -114,6 +120,8 @@ public class Cli extends LoggingAwareCommand {
checkConnection(cliSession, cliTerminal, con);
}
new CliRepl(cliTerminal, cliSession, cliCommand).execute();
} finally {
cliTerminal.close();
}
}
@ -138,6 +146,5 @@ public class Cli extends LoggingAwareCommand {
". This version of CLI only works with Elasticsearch version " + Version.CURRENT.toString());
}
}
}
}

View File

@ -68,6 +68,10 @@ public class ConnectionBuilder {
Path p = Paths.get(keystoreLocation);
checkIfExists("keystore file", p);
String keystorePassword = cliTerminal.readPassword("keystore password: ");
if (keystorePassword == null) {
throw new FatalCliException("readPassword shouldn't ever return null but ["
+ cliTerminal + "#readPassword] did!");
}
/*
* Set both the keystore and truststore settings which is required
@ -87,6 +91,10 @@ public class ConnectionBuilder {
if (user != null) {
if (password == null) {
password = cliTerminal.readPassword("password: ");
if (password == null) {
throw new FatalCliException("readPassword shouldn't ever return null but ["
+ cliTerminal + "#readPassword] did!");
}
}
properties.setProperty(ConnectionConfiguration.AUTH_USER, user);
properties.setProperty(ConnectionConfiguration.AUTH_PASS, password);

View File

@ -10,12 +10,10 @@ import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.UserInterruptException;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.InfoCmp;
import java.io.BufferedReader;
import java.io.IOException;
import static org.jline.utils.AttributedStyle.BOLD;
@ -32,15 +30,30 @@ public class JLineTerminal implements CliTerminal {
private Terminal terminal;
private LineReader reader;
protected JLineTerminal() {
try {
this.terminal = TerminalBuilder.builder().build();
reader = LineReaderBuilder.builder()
.terminal(terminal)
.completer(Completers.INSTANCE)
.build();
} catch (IOException ex) {
throw new FatalCliException("Cannot use terminal", ex);
/**
* Build the terminal.
* @param terminal the jLine terminal to work with
* @param enableMatchBracket should jLine bounce the cursor to matching brackets?
* this is disabled in tests because it very difficult to predict and
* enabled in production because it is fairly nice.
*/
public JLineTerminal(Terminal terminal, boolean enableMatchBracket) {
this(terminal,
LineReaderBuilder.builder()
.terminal(terminal)
.completer(Completers.INSTANCE)
.build(),
enableMatchBracket);
}
/**
* Constructor for tests.
*/
JLineTerminal(Terminal terminal, LineReader reader, boolean enableMatchBracket) {
this.terminal = terminal;
this.reader = reader;
if (false == enableMatchBracket) {
reader.setVariable(LineReader.BLINK_MATCHING_PAREN, 0L);
}
}
@ -91,23 +104,26 @@ public class JLineTerminal implements CliTerminal {
@Override
public String readPassword(String prompt) {
terminal.writer().print(prompt);
terminal.writer().flush();
terminal.echo(false);
try {
return new BufferedReader(terminal.reader()).readLine();
} catch (IOException ex) {
throw new FatalCliException("Error reading password", ex);
} finally {
terminal.echo(true);
String line = readLine(prompt, (char) 0);
if (line == null) {
throw new FatalCliException("Error reading password, terminal is closed");
}
return line;
}
@Override
public String readLine(String prompt) {
String attributedString = new AttributedString(prompt, DEFAULT.foreground(YELLOW)).toAnsi(terminal);
return readLine(attributedString, null);
}
private String readLine(String prompt, Character mask) {
try {
String attributedString = new AttributedString(prompt, DEFAULT.foreground(YELLOW)).toAnsi(terminal);
return reader.readLine(attributedString);
String line = reader.readLine(prompt, null, mask, null);
if (line == null) {
throw new FatalCliException("Error reading password, terminal is closed");
}
return line;
} catch (UserInterruptException ex) {
return "";
} catch (EndOfFileException ex) {

View File

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.cli;
import org.elasticsearch.test.ESTestCase;
import org.jline.reader.LineReader;
import org.jline.terminal.Terminal;
import java.io.IOException;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
public class JLineTerminalTests extends ESTestCase {
public void testReadPasswordSuccess() throws IOException {
Terminal wrapped = mock(Terminal.class);
LineReader reader = mock(LineReader.class);
try (JLineTerminal terminal = new JLineTerminal(wrapped, reader, randomBoolean())) {
String prompt = randomAlphaOfLength(5);
String expected = randomAlphaOfLength(5);
when(reader.readLine(prompt, null, (char) 0, null)).thenReturn(expected);
String actual = terminal.readPassword(prompt);
assertEquals(expected, actual);
verify(reader).readLine(prompt, null, (char) 0, null);
}
}
public void testReadPasswordEof() throws IOException {
Terminal wrapped = mock(Terminal.class);
LineReader reader = mock(LineReader.class);
try (JLineTerminal terminal = new JLineTerminal(wrapped, reader, randomBoolean())) {
String prompt = randomAlphaOfLength(5);
Exception e = expectThrows(FatalCliException.class, () -> terminal.readPassword(prompt));
assertEquals("Error reading password, terminal is closed", e.getMessage());
verify(reader).readLine(prompt, null, (char) 0, null);
}
}
public void testDisableMatchBracket() throws IOException {
Terminal wrapped = mock(Terminal.class);
LineReader reader = mock(LineReader.class);
new JLineTerminal(wrapped, reader, false).close();
verify(reader).setVariable(LineReader.BLINK_MATCHING_PAREN, 0L);
}
}

View File

@ -8,17 +8,13 @@ archivesBaseName = 'qa-sql'
dependencies {
compile "org.elasticsearch.test:framework:${version}"
// TODO: Restore shading when https://github.com/elastic/elasticsearch/pull/27955 gets in
testCompile "org.elasticsearch.plugin:x-pack-core:${version}"
// JDBC testing dependencies
// TODO: Restore shading when https://github.com/elastic/elasticsearch/issues/28504 is fixed
compile xpackProject('plugin:sql:jdbc')
compile "net.sourceforge.csvjdbc:csvjdbc:1.0.34"
runtime "com.h2database:h2:1.4.194"
// used for running debug tests
runtime 'org.antlr:antlr4-runtime:4.5.3'
/* There are *no* CLI testing dependencies because we
* communicate with a fixture to fork a new CLI process
* when we need it. */
compile project(path: xpackModule('sql:sql-cli'), configuration: 'nodeps')
compile "org.jline:jline:3.6.0"
}
/* disable unit tests because these are all integration tests used
@ -34,30 +30,50 @@ forbiddenApisMain {
}
thirdPartyAudit.excludes = [
// H2 dependencies that we don't actually use....
'javax.servlet.ServletConfig',
'javax.servlet.ServletContext',
'javax.servlet.ServletContextEvent',
'javax.servlet.ServletContextListener',
'javax.servlet.ServletOutputStream',
'javax.servlet.http.HttpServlet',
'javax.servlet.http.HttpServletRequest',
'javax.servlet.http.HttpServletResponse',
'org.apache.lucene.document.Field$Index',
'org.apache.lucene.queryParser.QueryParser',
'org.osgi.framework.BundleActivator',
'org.osgi.framework.BundleContext',
'org.osgi.service.jdbc.DataSourceFactory',
'org.slf4j.Logger',
'org.slf4j.LoggerFactory',
]
// jLine's optional dependencies
'org.apache.sshd.client.SshClient',
'org.apache.sshd.client.auth.keyboard.UserInteraction',
'org.apache.sshd.client.channel.ChannelShell',
'org.apache.sshd.client.channel.ClientChannel',
'org.apache.sshd.client.channel.ClientChannelEvent',
'org.apache.sshd.client.future.AuthFuture',
'org.apache.sshd.client.future.ConnectFuture',
'org.apache.sshd.client.future.OpenFuture',
'org.apache.sshd.client.session.ClientSession',
'org.apache.sshd.common.Factory',
'org.apache.sshd.common.channel.PtyMode',
'org.apache.sshd.common.config.keys.FilePasswordProvider',
'org.apache.sshd.common.util.io.NoCloseInputStream',
'org.apache.sshd.common.util.io.NoCloseOutputStream',
'org.apache.sshd.server.Command',
'org.apache.sshd.server.Environment',
'org.apache.sshd.server.ExitCallback',
'org.apache.sshd.server.SessionAware',
'org.apache.sshd.server.Signal',
'org.apache.sshd.server.SshServer',
'org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider',
'org.apache.sshd.server.scp.ScpCommandFactory$Builder',
'org.apache.sshd.server.session.ServerSession',
'org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory$Builder',
'org.fusesource.jansi.Ansi',
'org.fusesource.jansi.internal.CLibrary$Termios',
'org.fusesource.jansi.internal.CLibrary$WinSize',
'org.fusesource.jansi.internal.CLibrary',
'org.fusesource.jansi.internal.Kernel32$CHAR_INFO',
'org.fusesource.jansi.internal.Kernel32$CONSOLE_SCREEN_BUFFER_INFO',
'org.fusesource.jansi.internal.Kernel32$COORD',
'org.fusesource.jansi.internal.Kernel32$FOCUS_EVENT_RECORD',
'org.fusesource.jansi.internal.Kernel32$INPUT_RECORD',
'org.fusesource.jansi.internal.Kernel32$KEY_EVENT_RECORD',
'org.fusesource.jansi.internal.Kernel32$MOUSE_EVENT_RECORD',
'org.fusesource.jansi.internal.Kernel32$SMALL_RECT',
'org.fusesource.jansi.internal.Kernel32',
'org.fusesource.jansi.internal.WindowsSupport',
'org.mozilla.universalchardet.UniversalDetector',
]
subprojects {
apply plugin: 'elasticsearch.standalone-rest-test'
configurations {
cliFixture
cliJar
}
dependencies {
/* Since we're a standalone rest test we actually get transitive
* dependencies but we don't really want them because they cause
@ -69,51 +85,34 @@ subprojects {
testCompile "org.elasticsearch.test:framework:${version}"
// JDBC testing dependencies
testRuntime(xpackProject('plugin:sql:jdbc'))
// TODO: Restore shading when https://github.com/elastic/elasticsearch/pull/27955 gets in
// TODO: Restore shading when https://github.com/elastic/elasticsearch/issues/28504 is fixed
testRuntime xpackProject('plugin:sql:jdbc')
testRuntime("net.sourceforge.csvjdbc:csvjdbc:1.0.34") {
transitive = false
}
testRuntime("com.h2database:h2:1.4.194") {
transitive = false
}
// TODO check if needed
testRuntime("org.antlr:antlr4-runtime:4.5.3") {
transitive = false
}
cliFixture xpackProject('test:sql-cli-fixture')
cliJar(xpackProject('plugin:sql:sql-cli')) {
// We only need the jar file because it bundles all of its dependencies
transitive = false
}
testRuntime project(path: xpackModule('sql:sql-cli'), configuration: 'nodeps')
testRuntime "org.jline:jline:3.6.0"
}
if (project.name != 'security') {
// The security project just configures its subprojects
apply plugin: 'elasticsearch.rest-test'
task cliFixture(type: org.elasticsearch.gradle.test.AntFixture) {
Project cli = xpackProject('plugin:sql:sql-cli')
dependsOn project.configurations.cliFixture
dependsOn project.configurations.cliJar
executable = new File(project.runtimeJavaHome, 'bin/java')
env 'CLASSPATH', "${ -> project.configurations.cliFixture.asPath }"
args 'org.elasticsearch.xpack.sql.cli.fixture.CliFixture',
baseDir, "${ -> project.configurations.cliJar.singleFile}"
}
integTestCluster {
plugin xpackProject('plugin').path
setting 'xpack.monitoring.enabled', 'false'
setting 'xpack.ml.enabled', 'false'
setting 'xpack.watcher.enabled', 'false'
setting 'script.max_compilations_rate', '1000/1m'
dependsOn cliFixture
}
integTestRunner {
systemProperty 'tests.cli.fixture', "${ -> cliFixture.addressAndPort }"
finalizedBy cliFixture.stopTask
}
task runqa(type: RunTask) {
@ -122,8 +121,6 @@ subprojects {
setting 'xpack.ml.enabled', 'false'
setting 'xpack.watcher.enabled', 'false'
setting 'script.max_compilations_rate', '1000/1m'
dependsOn cliFixture
}
runqa.finalizedBy cliFixture.stopTask
}
}

View File

@ -7,6 +7,7 @@ package org.elasticsearch.xpack.qa.sql.security;
import org.elasticsearch.common.CheckedConsumer;
import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.xpack.qa.sql.cli.ErrorsTestCase;
import org.elasticsearch.xpack.qa.sql.cli.RemoteCli;
import org.elasticsearch.xpack.qa.sql.cli.RemoteCli.SecurityConfig;
import java.io.IOException;
@ -82,8 +83,8 @@ public class CliSecurityIT extends SqlSecurityTestCase {
@Override
public void expectScrollMatchesAdmin(String adminSql, String user, String userSql) throws Exception {
expectMatchesAdmin(adminSql, user, userSql, cli -> {
assertEquals("fetch size set to [90m1[0m", cli.command("fetch size = 1"));
assertEquals("fetch separator set to \"[90m -- fetch sep -- [0m\"",
assertEquals("[?1l>[?1000l[?2004lfetch size set to [90m1[0m", cli.command("fetch size = 1"));
assertEquals("[?1l>[?1000l[?2004lfetch separator set to \"[90m -- fetch sep -- [0m\"",
cli.command("fetch separator = \" -- fetch sep -- \""));
});
}
@ -157,7 +158,7 @@ public class CliSecurityIT extends SqlSecurityTestCase {
@Override
public void expectUnknownIndex(String user, String sql) throws Exception {
try (RemoteCli cli = new RemoteCli(elasticsearchAddress(), true, userSecurity(user))) {
assertThat(cli.command(sql), containsString("Bad request"));
ErrorsTestCase.assertFoundOneProblem(cli.command(sql));
assertThat(cli.readLine(), containsString("Unknown index"));
}
}
@ -176,13 +177,14 @@ public class CliSecurityIT extends SqlSecurityTestCase {
@Override
public void expectUnknownColumn(String user, String sql, String column) throws Exception {
try (RemoteCli cli = new RemoteCli(elasticsearchAddress(), true, userSecurity(user))) {
assertThat(cli.command(sql), containsString("[1;31mBad request"));
assertThat(cli.readLine(), containsString("Unknown column [" + column + "][1;23;31m][0m"));
ErrorsTestCase.assertFoundOneProblem(cli.command(sql));
assertThat(cli.readLine(), containsString("Unknown column [" + column + "]" + ErrorsTestCase.END));
}
}
@Override
public void checkNoMonitorMain(String user) throws Exception {
// Building the cli will attempt the connection and run the assertion
@SuppressWarnings("resource") // forceClose will close it
RemoteCli cli = new RemoteCli(elasticsearchAddress(), true, userSecurity(user)) {
@Override

View File

@ -70,6 +70,11 @@ public abstract class CliIntegrationTestCase extends ESRestTestCase {
return cli.command(command);
}
/**
* Read a line produced by the CLI.
* Note that these lines will contain {@code xterm-256color}
* escape sequences.
*/
public String readLine() throws IOException {
return cli.readLine();
}

View File

@ -17,16 +17,25 @@ import static org.hamcrest.Matchers.startsWith;
* Tests for error messages.
*/
public abstract class ErrorsTestCase extends CliIntegrationTestCase implements org.elasticsearch.xpack.qa.sql.ErrorsTestCase {
/**
* Starting sequence commons to lots of errors.
*/
public static final String START = "[?1l>[?1000l[?2004l[31;1m";
/**
* Ending sequence common to lots of errors.
*/
public static final String END = "[23;31;1m][0m";
@Override
public void testSelectInvalidSql() throws Exception {
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)", command("SELECT * FRO"));
assertEquals("line 1:8: Cannot determine columns for *[1;23;31m][0m", readLine());
assertFoundOneProblem(command("SELECT * FRO"));
assertEquals("line 1:8: Cannot determine columns for *" + END, readLine());
}
@Override
public void testSelectFromMissingIndex() throws IOException {
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)", command("SELECT * FROM test"));
assertEquals("line 1:15: Unknown index [test][1;23;31m][0m", readLine());
assertFoundOneProblem(command("SELECT * FROM test"));
assertEquals("line 1:15: Unknown index [test]" + END, readLine());
}
@Override
@ -34,59 +43,61 @@ public abstract class ErrorsTestCase extends CliIntegrationTestCase implements o
// Create an index without any types
client().performRequest("PUT", "/test", emptyMap(), new StringEntity("{}", ContentType.APPLICATION_JSON));
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)", command("SELECT * FROM test"));
assertEquals("line 1:15: [test] doesn't have any types so it is incompatible with sql[1;23;31m][0m", readLine());
assertFoundOneProblem(command("SELECT * FROM test"));
assertEquals("line 1:15: [test] doesn't have any types so it is incompatible with sql" + END, readLine());
}
@Override
public void testSelectMissingField() throws IOException {
index("test", body -> body.field("test", "test"));
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)", command("SELECT missing FROM test"));
assertEquals("line 1:8: Unknown column [missing][1;23;31m][0m", readLine());
assertFoundOneProblem(command("SELECT missing FROM test"));
assertEquals("line 1:8: Unknown column [missing]" + END, readLine());
}
@Override
public void testSelectMissingFunction() throws Exception {
index("test", body -> body.field("foo", 1));
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)", command("SELECT missing(foo) FROM test"));
assertEquals("line 1:8: Unknown function [missing][1;23;31m][0m", readLine());
assertFoundOneProblem(command("SELECT missing(foo) FROM test"));
assertEquals("line 1:8: Unknown function [missing]" + END, readLine());
}
@Override
public void testSelectProjectScoreInAggContext() throws Exception {
index("test", body -> body.field("foo", 1));
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)", command("SELECT foo, SCORE(), COUNT(*) FROM test GROUP BY foo"));
assertEquals("line 1:13: Cannot use non-grouped column [SCORE()], expected [foo][1;23;31m][0m", readLine());
assertFoundOneProblem(command("SELECT foo, SCORE(), COUNT(*) FROM test GROUP BY foo"));
assertEquals("line 1:13: Cannot use non-grouped column [SCORE()], expected [foo]" + END, readLine());
}
@Override
public void testSelectOrderByScoreInAggContext() throws Exception {
index("test", body -> body.field("foo", 1));
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)",
command("SELECT foo, COUNT(*) FROM test GROUP BY foo ORDER BY SCORE()"));
assertEquals("line 1:54: Cannot order by non-grouped column [SCORE()], expected [foo][1;23;31m][0m", readLine());
assertFoundOneProblem(command("SELECT foo, COUNT(*) FROM test GROUP BY foo ORDER BY SCORE()"));
assertEquals("line 1:54: Cannot order by non-grouped column [SCORE()], expected [foo]" + END, readLine());
}
@Override
public void testSelectGroupByScore() throws Exception {
index("test", body -> body.field("foo", 1));
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)",
command("SELECT COUNT(*) FROM test GROUP BY SCORE()"));
assertEquals("line 1:36: Cannot use [SCORE()] for grouping[1;23;31m][0m", readLine());
assertFoundOneProblem(command("SELECT COUNT(*) FROM test GROUP BY SCORE()"));
assertEquals("line 1:36: Cannot use [SCORE()] for grouping" + END, readLine());
}
@Override
public void testSelectScoreSubField() throws Exception {
index("test", body -> body.field("foo", 1));
assertThat(command("SELECT SCORE().bar FROM test"),
startsWith("[1;31mBad request [[22;3;33mline 1:15: extraneous input '.' expecting {<EOF>, ',',"));
startsWith(START + "Bad request [[3;33;22mline 1:15: extraneous input '.' expecting {<EOF>, ',',"));
}
@Override
public void testSelectScoreInScalar() throws Exception {
index("test", body -> body.field("foo", 1));
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)",
command("SELECT SIN(SCORE()) FROM test"));
assertEquals("line 1:12: [SCORE()] cannot be an argument to a function[1;23;31m][0m", readLine());
assertFoundOneProblem(command("SELECT SIN(SCORE()) FROM test"));
assertEquals("line 1:12: [SCORE()] cannot be an argument to a function" + END, readLine());
}
public static void assertFoundOneProblem(String commandResult) {
assertEquals(START + "Bad request [[3;33;22mFound 1 problem(s)", commandResult);
}
}

View File

@ -25,8 +25,9 @@ public abstract class FetchSizeTestCase extends CliIntegrationTestCase {
}
client().performRequest("PUT", "/test/doc/_bulk", singletonMap("refresh", "true"),
new StringEntity(bulk.toString(), ContentType.APPLICATION_JSON));
assertEquals("fetch size set to [90m4[0m", command("fetch size = 4"));
assertEquals("fetch separator set to \"[90m -- fetch sep -- [0m\"", command("fetch separator = \" -- fetch sep -- \""));
assertEquals("[?1l>[?1000l[?2004lfetch size set to [90m4[0m", command("fetch size = 4"));
assertEquals("[?1l>[?1000l[?2004lfetch separator set to \"[90m -- fetch sep -- [0m\"",
command("fetch separator = \" -- fetch sep -- \""));
assertThat(command("SELECT * FROM test ORDER BY test_field ASC"), containsString("test_field"));
assertThat(readLine(), containsString("----------"));
int i = 0;
@ -41,9 +42,10 @@ public abstract class FetchSizeTestCase extends CliIntegrationTestCase {
}
public void testInvalidFetchSize() throws IOException {
assertEquals("[1;31mInvalid fetch size [[22;3;33mcat[1;23;31m][0m", command("fetch size = cat"));
assertEquals("[1;31mInvalid fetch size [[22;3;33m0[1;23;31m]. Must be > 0.[0m", command("fetch size = 0"));
assertEquals("[1;31mInvalid fetch size [[22;3;33m-1231[1;23;31m]. Must be > 0.[0m", command("fetch size = -1231"));
assertEquals("[1;31mInvalid fetch size [[22;3;33m" + Long.MAX_VALUE + "[1;23;31m][0m", command("fetch size = " + Long.MAX_VALUE));
assertEquals(ErrorsTestCase.START + "Invalid fetch size [[3;33;22mcat" + ErrorsTestCase.END, command("fetch size = cat"));
assertEquals(ErrorsTestCase.START + "Invalid fetch size [[3;33;22m0[23;31;1m]. Must be > 0.[0m", command("fetch size = 0"));
assertEquals(ErrorsTestCase.START + "Invalid fetch size [[3;33;22m-1231[23;31;1m]. Must be > 0.[0m", command("fetch size = -1231"));
assertEquals(ErrorsTestCase.START + "Invalid fetch size [[3;33;22m" + Long.MAX_VALUE + ErrorsTestCase.END,
command("fetch size = " + Long.MAX_VALUE));
}
}

View File

@ -6,16 +6,25 @@
package org.elasticsearch.xpack.qa.sql.cli;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.SpecialPermission;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.cli.MockTerminal;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.xpack.sql.cli.Cli;
import org.elasticsearch.xpack.sql.cli.CliTerminal;
import org.elasticsearch.xpack.sql.cli.JLineTerminal;
import org.jline.terminal.impl.ExternalTerminal;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
@ -24,7 +33,13 @@ import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import static org.elasticsearch.test.ESTestCase.randomBoolean;
@ -34,101 +49,128 @@ import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
/**
* Wraps a CLI in as "real" a way as it can get without forking the CLI
* subprocess with the goal being integration testing of the CLI without
* breaking out security model by forking. We test the script that starts
* the CLI using packaging tests which is super "real" but not super fast
* and doesn't run super frequently.
*/
public class RemoteCli implements Closeable {
// TODO rename this class to EmbeddedCli very soon
private static final Logger logger = Loggers.getLogger(RemoteCli.class);
private static final InetAddress CLI_FIXTURE_ADDRESS;
private static final int CLI_FIXTURE_PORT;
static {
String addressAndPort = System.getProperty("tests.cli.fixture");
if (addressAndPort == null) {
throw new IllegalArgumentException("Must set the [tests.cli.fixture] property. Gradle handles this for you "
+ " in regular tests. In embedded mode the easiest thing to do is run "
+ "`gradle :x-pack-elasticsearch:qa:sql:no-security:run` and to set the property to the contents of "
+ "`qa/sql/no-security/build/fixtures/cliFixture/ports`");
}
int split = addressAndPort.lastIndexOf(':');
try {
CLI_FIXTURE_ADDRESS = InetAddress.getByName(addressAndPort.substring(0, split));
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
CLI_FIXTURE_PORT = Integer.parseInt(addressAndPort.substring(split + 1));
}
private final Socket socket;
private final PrintWriter out;
private final Thread exec;
private final Cli cli;
private final AtomicInteger returnCode = new AtomicInteger(Integer.MIN_VALUE);
private final AtomicReference<Exception> failure = new AtomicReference<>();
private final BufferedWriter out;
private final BufferedReader in;
/**
* Has the client already been closed?
*/
private boolean closed = false;
public RemoteCli(String elasticsearchAddress, boolean checkConnectionOnStartup,
@Nullable SecurityConfig security) throws IOException {
// Connect
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
logger.info("connecting to the cli fixture at {}:{}", CLI_FIXTURE_ADDRESS, CLI_FIXTURE_PORT);
socket = AccessController.doPrivileged(new PrivilegedAction<Socket>() {
PipedOutputStream outgoing = new PipedOutputStream();
PipedInputStream cliIn = new PipedInputStream(outgoing);
PipedInputStream incoming = new PipedInputStream();
PipedOutputStream cliOut = new PipedOutputStream(incoming);
CliTerminal cliTerminal = new JLineTerminal(
new ExternalTerminal("test", "xterm-256color", cliIn, cliOut, StandardCharsets.UTF_8),
false);
cli = new Cli(cliTerminal) {
@Override
public Socket run() {
try {
return new Socket(CLI_FIXTURE_ADDRESS, CLI_FIXTURE_PORT);
} catch (IOException e) {
throw new RuntimeException(e);
}
protected boolean addShutdownHook() {
return false;
}
};
out = new BufferedWriter(new OutputStreamWriter(outgoing, StandardCharsets.UTF_8));
in = new BufferedReader(new InputStreamReader(incoming, StandardCharsets.UTF_8));
List<String> args = new ArrayList<>();
if (security == null) {
args.add(elasticsearchAddress);
} else {
String address = security.user + "@" + elasticsearchAddress;
if (security.https) {
address = "https://" + address;
} else if (randomBoolean()) {
address = "http://" + address;
}
args.add(address);
if (security.keystoreLocation != null) {
args.add("-keystore_location");
args.add(security.keystoreLocation);
}
}
if (false == checkConnectionOnStartup) {
args.add("-check");
args.add("false");
}
args.add("-debug");
exec = new Thread(() -> {
try {
/*
* We don't really interact with the terminal because we're
* trying to test our interaction with jLine which doesn't
* support Elasticsearch's Terminal abstraction.
*/
Terminal terminal = new MockTerminal();
int exitCode = cli.main(args.toArray(new String[0]), terminal);
returnCode.set(exitCode);
logger.info("cli exited with code [{}]", exitCode);
} catch (Exception e) {
failure.set(e);
}
});
logger.info("connected");
socket.setSoTimeout(10000);
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
exec.start();
try {
// Start the CLI
String command;
if (security == null) {
command = elasticsearchAddress;
} else {
command = security.user + "@" + elasticsearchAddress;
if (security.https) {
command = "https://" + command;
} else if (randomBoolean()) {
command = "http://" + command;
}
if (security.keystoreLocation != null) {
command = command + " -keystore_location " + security.keystoreLocation;
}
}
if (false == checkConnectionOnStartup) {
command += " -check false";
}
/* Don't use println because it emits \r\n on windows but we put the
* terminal in unix mode to make the tests consistent. */
out.print(command + "\n");
out.flush();
// Feed it passwords if needed
if (security != null && security.keystoreLocation != null) {
assertEquals("keystore password: ", readUntil(s -> s.endsWith(": ")));
out.print(security.keystorePassword + "\n");
out.flush();
}
if (security != null) {
assertEquals("password: ", readUntil(s -> s.endsWith(": ")));
out.print(security.password + "\n");
String passwordPrompt = "[?1h=[?2004hpassword: ";
if (security.keystoreLocation != null) {
assertEquals("[?1h=[?2004hkeystore password: ", readUntil(s -> s.endsWith(": ")));
out.write(security.keystorePassword + "\n");
out.flush();
logger.info("out: {}", security.keystorePassword);
// Read the newline echoed after the password prompt
assertEquals("", readLine());
/*
* And for some reason jLine adds a second one so
* consume that too. I'm not sure why it does this
* but it looks right when a use runs the cli.
*/
assertEquals("", readLine());
/*
* If we read the keystore password the console will
* emit some state reset escape sequences before the
* prompt for the password.
*/
passwordPrompt = "[?1l>[?1000l[?2004l[?1h=[?2004hpassword: ";
}
assertEquals(passwordPrompt, readUntil(s -> s.endsWith(": ")));
out.write(security.password + "\n");
out.flush();
logger.info("out: {}", security.password);
// Read the newline echoed after the password prompt
assertEquals("", readLine());
}
// Throw out the logo and warnings about making a dumb terminal
// Throw out the logo
while (false == readLine().contains("SQL"));
assertConnectionTest();
} catch (AssertionError | Exception e) {
/* If there is an error during connection then try and
* force the socket shut. */
forceClose();
throw e;
} catch (IOException e) {
try {
forceClose();
} catch (Exception closeException) {
e.addSuppressed(closeException);
throw e;
}
}
}
@ -147,15 +189,33 @@ public class RemoteCli implements Closeable {
*/
@Override
public void close() throws IOException {
if (closed) {
return;
}
try {
// Try and shutdown the client normally
/* Don't use println because it enits \r\n on windows but we put the
* terminal in unix mode to make the tests consistent. */
out.print("quit;\n");
/*
* Don't use command here because we want want
* to collect all the responses and report them
* as failures if there is a problem rather than
* failing on the first bad response.
*/
out.write("quit;\n");
out.flush();
List<String> nonQuit = new ArrayList<>();
String line;
while (false == (line = readLine()).startsWith("[?1h=[33msql> [0mquit;[90mBye![0m")) {
while (true) {
line = readLine();
if (line == null) {
fail("got EOF before [Bye!]. Extras " + nonQuit);
}
if (line.contains("quit;")) {
continue;
}
if (line.contains("Bye!")) {
break;
}
if (false == line.isEmpty()) {
nonQuit.add(line);
}
@ -164,6 +224,7 @@ public class RemoteCli implements Closeable {
} finally {
forceClose();
}
assertEquals(0, returnCode.get());
}
/**
@ -171,10 +232,18 @@ public class RemoteCli implements Closeable {
* the remote down in an orderly way.
*/
public void forceClose() throws IOException {
out.close();
in.close();
// Most importantly, close the socket so the next test can use the fixture
socket.close();
closed = true;
IOUtils.close(out, in, cli);
try {
exec.join(TimeUnit.SECONDS.toMillis(10));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
Exception e = failure.get();
if (e != null) {
throw new RuntimeException("CLI thread failed", e);
}
}
/**
@ -183,22 +252,42 @@ public class RemoteCli implements Closeable {
public String command(String command) throws IOException {
assertThat("; automatically added", command, not(endsWith(";")));
logger.info("out: {};", command);
/* Don't use println because it emits \r\n on windows but we put the
* terminal in unix mode to make the tests consistent. */
out.print(command + ";\n");
out.write(command + ";\n");
out.flush();
final String firstResponse = "[?1h=[33msql> [0m" +
Strings.collectionToDelimitedString(Strings.splitSmart(command, "\n", false), "[?1h=[33m | [0m") + ";";
String firstLine = readLine();
assertThat(firstLine, startsWith(firstResponse));
return firstLine.substring(firstResponse.length());
for (String echo : expectedCommandEchos(command)) {
assertEquals(echo, readLine());
}
return readLine();
}
/**
* Create the "echo" that we expect jLine to send to the terminal
* while we're typing a command.
*/
private List<String> expectedCommandEchos(String command) {
List<String> commandLines = Strings.splitSmart(command, "\n", false);
List<String> result = new ArrayList<>(commandLines.size() * 2);
result.add("[?1h=[?2004h[33msql> [0m" + commandLines.get(0));
// Every line gets an extra new line because, I dunno, but it looks right in the CLI
result.add("");
for (int i = 1; i < commandLines.size(); i++) {
result.add("[?1l>[?1000l[?2004l[?1h=[?2004h[33m | [0m" + commandLines.get(i));
// Every line gets an extra new line because, I dunno, but it looks right in the CLI
result.add("");
}
result.set(result.size() - 2, result.get(result.size() - 2) + ";");
return result;
}
public String readLine() throws IOException {
/* Since we can't *see* esc in the error messages we just
/*
* Since we can't *see* esc in the error messages we just
* remove it here and pretend it isn't required. Hopefully
* `[` is enough for us to assert on. */
String line = in.readLine().replace("\u001B", "");
* `[` is enough for us to assert on.
*
* `null` means EOF so we should just pass that back through.
*/
String line = in.readLine();
line = line == null ? null : line.replace("\u001B", "");
logger.info("in : {}", line);
return line;
}
@ -206,14 +295,25 @@ public class RemoteCli implements Closeable {
private String readUntil(Predicate<String> end) throws IOException {
StringBuilder b = new StringBuilder();
String result;
do {
while (true) {
int c = in.read();
if (c == -1) {
throw new IOException("got eof before end");
}
if (c == '\u001B') {
/*
* Since we can't *see* esc in the error messages we just
* remove it here and pretend it isn't required. Hopefully
* `[` is enough for us to assert on.
*/
continue;
}
b.append((char) c);
result = b.toString();
} while (false == end.test(result));
if (end.test(result)) {
break;
}
}
logger.info("in : {}", result);
return result;
}

View File

@ -1,14 +0,0 @@
apply plugin: 'elasticsearch.build'
forbiddenApisMain {
// does not depend on core, so only jdk and http signatures should be checked
signaturesURLs = [this.class.getResource('/forbidden/jdk-signatures.txt')]
}
thirdPartyAudit.enabled = false
licenseHeaders.enabled = false
test.enabled = false
jarHell.enabled = false
// Not published so no need to assemble
tasks.remove(assemble)
build.dependsOn.remove('assemble')

View File

@ -1,154 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.cli.fixture;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import static java.util.Collections.singleton;
public class CliFixture {
public static void main(String[] args) throws IOException, InterruptedException {
if (args.length < 2) {
throw new IllegalArgumentException("usage: <logdir> <clijar>");
}
Path dir = Paths.get(args[0]);
Path cliJar = Paths.get(args[1]);
int port = 0;
if (args.length > 2) {
port = Integer.parseInt(args[2]);
}
if (false == Files.exists(cliJar)) {
throw new IllegalArgumentException(cliJar + " doesn't exist");
}
if (false == Files.isRegularFile(cliJar)) {
throw new IllegalArgumentException(cliJar + " is not a regular file");
}
String javaExec = "java";
boolean isWindows = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win");
if (isWindows) {
javaExec += ".exe";
}
Path javaExecutable = Paths.get(System.getProperty("java.home"), "bin", javaExec);
if (false == Files.exists(javaExecutable)) {
throw new IllegalArgumentException(javaExec + " doesn't exist");
}
if (false == Files.isExecutable(javaExecutable)) {
throw new IllegalArgumentException(javaExec + " isn't executable");
}
try (ServerSocket server = new ServerSocket()) {
server.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), port));
// write pid file
Path tmp = Files.createTempFile(dir, null, null);
String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
Files.write(tmp, Collections.singleton(pid));
Files.move(tmp, dir.resolve("pid"), StandardCopyOption.ATOMIC_MOVE);
// write port file
tmp = Files.createTempFile(dir, null, null);
InetSocketAddress bound = (InetSocketAddress) server.getLocalSocketAddress();
if (bound.getAddress() instanceof Inet6Address) {
Files.write(tmp, singleton("[" + bound.getHostString() + "]:" + bound.getPort()));
}
else {
Files.write(tmp, singleton(bound.getHostString() + ":" + bound.getPort()));
}
Files.move(tmp, dir.resolve("ports"), StandardCopyOption.ATOMIC_MOVE);
boolean run = true;
// Run forever until killed
while (run) {
try {
println("accepting on localhost:" + server.getLocalPort());
Socket s = server.accept();
String line = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8)).readLine();
if (line == null || line.isEmpty()) {
continue;
}
List<String> command = new ArrayList<>();
command.add(javaExecutable.toString());
// command.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000");
// Force a specific terminal type so we have consistent responses for testing.
command.add("-Dorg.jline.terminal.type=xterm-256color");
// Disable terminal types that won't work with stdin isn't actually a tty
command.add("-Dorg.jline.terminal.jna=false");
command.add("-Dorg.jline.terminal.jansi=false");
command.add("-Dorg.jline.terminal.exec=false");
command.add("-Dorg.jline.terminal.dumb=true");
command.add("-jar");
command.add(cliJar.toString());
command.add("-d");
command.addAll(Arrays.asList(line.split(" ")));
ProcessBuilder cliBuilder = new ProcessBuilder(command);
// Clear the environment to drop JAVA_TOOLS which prints strange things on startup
cliBuilder.environment().clear();
cliBuilder.redirectErrorStream(true);
Process process = cliBuilder.start();
println("started " + command);
new Thread(() -> {
int i;
try {
while ((i = process.getInputStream().read()) != -1) {
s.getOutputStream().write(i);
s.getOutputStream().flush();
}
} catch (IOException e) {
throw new RuntimeException("failed to copy from process to socket", e);
} finally {
process.destroyForcibly();
}
}).start();
new Thread(() -> {
int i;
try {
while ((i = s.getInputStream().read()) != -1) {
process.getOutputStream().write(i);
process.getOutputStream().flush();
}
} catch (IOException e) {
throw new RuntimeException("failed to copy from socket to process", e);
} finally {
process.destroyForcibly();
}
}).start();
process.waitFor();
} catch (IOException e) {
printStackTrace("error at the top level, continuing", e);
}
}
}
}
@SuppressForbidden(reason = "cli application")
private static void println(String line) {
System.out.println(line);
}
@SuppressForbidden(reason = "cli application")
private static void printStackTrace(String reason, Throwable t) {
System.err.println(reason);
t.printStackTrace();
}
}

View File

@ -1,17 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.cli.fixture;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
public @interface SuppressForbidden {
String reason();
}