From 903b7979de7aa3d2d340c157f78982b0231741ce Mon Sep 17 00:00:00 2001 From: Lai Jiang Date: Fri, 12 Apr 2024 15:57:02 -0400 Subject: [PATCH] Upgrade to jline 3 (#2400) jline 3 contains API breaking changes, necessitating changes in ShellCommand. --- core/build.gradle | 2 +- core/gradle.lockfile | 2 +- .../google/registry/tools/ShellCommand.java | 166 ++++++---------- .../registry/tools/ShellCommandTest.java | 188 ++++++++++-------- dependencies.gradle | 3 +- jetty/gradle.lockfile | 2 +- services/backend/gradle.lockfile | 2 +- services/bsa/gradle.lockfile | 2 +- services/default/gradle.lockfile | 2 +- services/pubapi/gradle.lockfile | 2 +- services/tools/gradle.lockfile | 2 +- 11 files changed, 180 insertions(+), 193 deletions(-) diff --git a/core/build.gradle b/core/build.gradle index 6a2ea5448..392fc986b 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -199,7 +199,6 @@ dependencies { implementation deps['javax.persistence:javax.persistence-api'] implementation deps['jakarta.servlet:jakarta.servlet-api'] implementation deps['javax.xml.bind:jaxb-api'] - implementation deps['jline:jline'] implementation deps['joda-time:joda-time'] implementation deps['org.apache.avro:avro'] testImplementation deps['org.apache.beam:beam-runners-core-construction-java'] @@ -226,6 +225,7 @@ dependencies { implementation deps['org.hibernate:hibernate-core'] implementation deps['org.hibernate:hibernate-hikaricp'] implementation deps['org.jcommander:jcommander'] + implementation deps['org.jline:jline'] implementation deps['org.joda:joda-money'] implementation deps['org.json:json'] implementation deps['org.jsoup:jsoup'] diff --git a/core/gradle.lockfile b/core/gradle.lockfile index 3301285d9..175f0253b 100644 --- a/core/gradle.lockfile +++ b/core/gradle.lockfile @@ -321,7 +321,6 @@ javax.servlet:servlet-api:2.5=compileClasspath,deploy_jar,nonprodCompileClasspat javax.validation:validation-api:1.0.0.GA=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.4.0-b180830.0359=jaxb -jline:jline:1.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath joda-time:joda-time:2.10.14=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath junit:junit:4.13.2=nonprodCompileClasspath,nonprodRuntimeClasspath,testCompileClasspath,testRuntimeClasspath net.arnx:nashorn-promise:0.1.1=testRuntimeClasspath @@ -442,6 +441,7 @@ org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.0.1=deploy_jar,nonprodRun org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.1=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath org.jetbrains:annotations:13.0=annotationProcessor,testAnnotationProcessor org.jetbrains:annotations:17.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jline:jline:3.25.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.joda:joda-money:1.0.4=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.json:json:20160212=soy org.json:json:20231013=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/core/src/main/java/google/registry/tools/ShellCommand.java b/core/src/main/java/google/registry/tools/ShellCommand.java index 8d390b1d0..88bd0fbd4 100644 --- a/core/src/main/java/google/registry/tools/ShellCommand.java +++ b/core/src/main/java/google/registry/tools/ShellCommand.java @@ -16,7 +16,6 @@ package google.registry.tools; import static com.google.common.base.StandardSystemProperty.USER_HOME; import static com.google.common.collect.ImmutableList.toImmutableList; -import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_8; import com.beust.jcommander.JCommander; @@ -31,27 +30,31 @@ import com.google.common.escape.Escaper; import com.google.common.escape.Escapers; import google.registry.util.Clock; import google.registry.util.SystemClock; -import java.io.BufferedReader; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.FilterOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.io.StreamTokenizer; import java.io.StringReader; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.Map.Entry; import java.util.Optional; import javax.annotation.Nullable; -import jline.Completor; -import jline.ConsoleReader; -import jline.ConsoleReaderInputStream; -import jline.FileNameCompletor; -import jline.History; +import org.jline.builtins.Completers.FileNameCompleter; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.ParsedLine; +import org.jline.reader.UserInterruptException; +import org.jline.reader.impl.LineReaderImpl; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.terminal.impl.DumbTerminal; import org.joda.time.DateTime; import org.joda.time.Duration; @@ -89,73 +92,56 @@ public class ShellCommand implements Command { */ private final CommandRunner originalRunner; - private final BufferedReader lineReader; - private final ConsoleReader consoleReader; + private final LineReader lineReader; private final Clock clock; + private String prompt = null; + @Parameter( names = {"--dont_exit_on_idle"}, description = - "Prevents the shell from exiting on PROD after the 1 hour idle delay. " - + "Will instead warn you and require re-running the command.") + """ + Prevents the shell from exiting on PROD after the 1 hour idle delay. + Will instead warn you and require re-running the command.""") boolean dontExitOnIdle = false; @Parameter( names = {"--encapsulate_output"}, description = - "Encapsulate command standard output and error by combining the two streams to standard " - + "output and inserting a prefix ('out:' or 'err:') at the beginning of every line " - + "of normal output and adding a line consisting of either 'SUCCESS' or " - + "'FAILURE ' at the end of the output for a " - + "command, allowing the output to be easily parsed by wrapper scripts.") + """ + Encapsulate command standard output and error by combining the two streams to + standard output and inserting a prefix ('out:' or 'err:') at the beginning of every + line of normal output and adding a line consisting of either 'SUCCESS' or + 'FAILURE ' at the end of the output for a + command, allowing the output to be easily parsed by wrapper scripts.""") boolean encapsulateOutput = false; ShellCommand(CommandRunner runner) throws IOException { - this.originalRunner = runner; - InputStream in = System.in; - if (System.console() != null) { - consoleReader = new ConsoleReader(); - // There are 104 different commands. We want the threshold to be more than that - consoleReader.setAutoprintThreshhold(200); - consoleReader.setDefaultPrompt("nom > "); - consoleReader.setHistory(new History(new File(USER_HOME.value(), HISTORY_FILE))); - in = new ConsoleReaderInputStream(consoleReader); - } else { - consoleReader = null; - } - this.lineReader = new BufferedReader(new InputStreamReader(in, US_ASCII)); - this.clock = new SystemClock(); + this(TerminalBuilder.terminal(), new SystemClock(), runner); + prompt = "nom > "; + lineReader.variable(LineReader.HISTORY_FILE, Path.of(USER_HOME.value(), HISTORY_FILE)); } - @VisibleForTesting - ShellCommand(BufferedReader bufferedReader, Clock clock, CommandRunner runner) { + ShellCommand(Terminal terminal, Clock clock, CommandRunner runner) { this.originalRunner = runner; - this.lineReader = bufferedReader; + this.lineReader = LineReaderBuilder.builder().terminal(terminal).build(); this.clock = clock; - this.consoleReader = null; } private void setPrompt(RegistryToolEnvironment environment, boolean alert) { - if (consoleReader == null) { + // Do not set the prompt in tests. + if (lineReader.getTerminal() instanceof DumbTerminal) { return; } - if (alert) { - consoleReader.setDefaultPrompt( - String.format("nom@%s%s%s > ", ALERT_COLOR, environment, RESET)); - } else { - consoleReader.setDefaultPrompt( - String.format( - "nom@%s%s%s > ", NON_ALERT_COLOR, Ascii.toLowerCase(environment.toString()), RESET)); - } + prompt = + alert + ? String.format("nom@%s%s%s > ", ALERT_COLOR, environment, RESET) + : String.format( + "nom@%s%s%s > ", NON_ALERT_COLOR, Ascii.toLowerCase(environment.toString()), RESET); } public ShellCommand buildCompletions(JCommander jcommander) { - if (consoleReader != null) { - @SuppressWarnings("unchecked") - ImmutableList completors = ImmutableList.copyOf(consoleReader.getCompletors()); - completors.forEach(consoleReader::removeCompletor); - consoleReader.addCompletor(new JCommanderCompletor(jcommander)); - } + ((LineReaderImpl) lineReader).setCompleter(new JCommanderCompleter(jcommander)); return this; } @@ -230,21 +216,28 @@ public class ShellCommand implements Command { setPrompt(RegistryToolEnvironment.get(), beExtraCareful); String line; DateTime lastTime = clock.nowUtc(); - while ((line = getLine()) != null) { + while (true) { + try { + line = lineReader.readLine(prompt); + } catch (UserInterruptException | EndOfFileException e) { + break; + } // Make sure we're not idle for too long. Only relevant when we're "extra careful" if (!dontExitOnIdle && beExtraCareful && lastTime.plus(IDLE_THRESHOLD).isBefore(clock.nowUtc())) { throw new RuntimeException( - "Been idle for too long, while in 'extra careful' mode. " - + "The last command was saved in history. Please rerun the shell and try again."); + """ + Been idle for too long, while in 'extra careful' mode. + The last command was saved in history. Please rerun the shell and try again."""); } lastTime = clock.nowUtc(); String[] lineArgs = parseCommand(line); if (lineArgs.length == 0) { continue; + } else if (lineArgs.length == 1 && "exit".equals(lineArgs[0])) { + break; } - try { runner.run(lineArgs); } catch (Exception e) { @@ -257,14 +250,6 @@ public class ShellCommand implements Command { } } - private String getLine() { - try { - return lineReader.readLine(); - } catch (IOException e) { - return null; - } - } - @VisibleForTesting static String[] parseCommand(String line) { ImmutableList.Builder resultBuilder = new ImmutableList.Builder<>(); @@ -288,7 +273,7 @@ public class ShellCommand implements Command { return resultBuilder.build().toArray(new String[0]); } - static class JCommanderCompletor implements Completor { + static class JCommanderCompleter implements Completer { private static final ParamDoc DEFAULT_PARAM_DOC = new ParamDoc("[No documentation available]", ImmutableList.of()); @@ -312,7 +297,7 @@ public class ShellCommand implements Command { */ private final ImmutableTable commandFlagDocs; - private final FileNameCompletor filenameCompletor = new FileNameCompletor(); + private final FileNameCompleter filenameCompleter = new FileNameCompleter(); /** * Holds all the information about a parameter we need for completion. @@ -351,9 +336,9 @@ public class ShellCommand implements Command { * Populates the completions and documentation based on the JCommander. * *

The input data is copied, so changing the jcommander after creation of the - * JCommanderCompletor doesn't change the completions. + * JCommanderCompleter doesn't change the completions. */ - JCommanderCompletor(JCommander jcommander) { + JCommanderCompleter(JCommander jcommander) { ImmutableTable.Builder builder = new ImmutableTable.Builder<>(); // Go over all the commands @@ -384,53 +369,26 @@ public class ShellCommand implements Command { } @Override - @SuppressWarnings("unchecked") - public int complete(String buffer, int location, List completions) { - // We just defer to the other function because of the warnings (the use of a naked List by - // jline) - return completeInternal(buffer, location, completions); - } - - /** - * Given a string, finds all the possible completions to the end of that string. - * - * @param buffer the command line. - * @param location the location in the command line we want to complete - * @param completions a list to fill with the completion results - * @return the number of characters back from the location that are part of the completions - */ - int completeInternal(String buffer, int location, List completions) { - String truncatedBuffer = buffer.substring(0, location); - String[] parsedBuffer = parseCommand(truncatedBuffer); - int argumentIndex = parsedBuffer.length - 1; - - if (argumentIndex < 0 || !truncatedBuffer.endsWith(parsedBuffer[argumentIndex])) { - argumentIndex += 1; - } + public void complete(LineReader reader, ParsedLine line, List candidates) { + int argumentIndex = line.wordIndex(); // The argument we want to complete (only partially written, might even be empty) - String partialArgument = - argumentIndex < parsedBuffer.length ? parsedBuffer[argumentIndex] : ""; - int argumentStart = location - partialArgument.length(); + String partialArgument = line.word(); // The command name. Null if we're at the first argument - String command = argumentIndex == 0 ? null : parsedBuffer[0]; + String command = argumentIndex == 0 ? null : line.words().getFirst(); // The previous argument before it - used for context. Null if we're at the first argument - String previousArgument = argumentIndex <= 1 ? null : parsedBuffer[argumentIndex - 1]; + String previousArgument = argumentIndex <= 1 ? null : line.words().get(argumentIndex - 1); // If it's obviously a file path (starts with something "file path like") - complete as a file if (partialArgument.startsWith("./") || partialArgument.startsWith("~/") || partialArgument.startsWith("/")) { - int offset = - filenameCompletor.complete(partialArgument, partialArgument.length(), completions); - if (offset >= 0) { - return argumentStart + offset; - } - return -1; + filenameCompleter.complete(reader, line, candidates); } // Complete based on flag data - completions.addAll(getCompletions(command, previousArgument, partialArgument)); - return argumentStart; + getCompletions(command, previousArgument, partialArgument).stream() + .map(Candidate::new) + .forEach(candidates::add); } /** diff --git a/core/src/test/java/google/registry/tools/ShellCommandTest.java b/core/src/test/java/google/registry/tools/ShellCommandTest.java index 71d98a29b..b9e4288a2 100644 --- a/core/src/test/java/google/registry/tools/ShellCommandTest.java +++ b/core/src/test/java/google/registry/tools/ShellCommandTest.java @@ -15,10 +15,9 @@ package google.registry.tools; import static com.google.common.truth.Truth.assertThat; -import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.charset.StandardCharsets.US_ASCII; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import com.beust.jcommander.JCommander; import com.beust.jcommander.MissingCommandException; @@ -28,15 +27,20 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import google.registry.testing.FakeClock; import google.registry.testing.SystemPropertyExtension; -import google.registry.tools.ShellCommand.JCommanderCompletor; -import java.io.BufferedReader; +import google.registry.tools.ShellCommand.JCommanderCompleter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.PrintStream; -import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import org.jline.reader.Candidate; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.impl.DefaultParser; +import org.jline.terminal.Terminal; +import org.jline.terminal.impl.DumbTerminal; import org.joda.time.DateTime; import org.joda.time.Duration; import org.junit.jupiter.api.AfterEach; @@ -52,6 +56,7 @@ class ShellCommandTest { CommandRunner cli = mock(CommandRunner.class); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ")); + private final DelayingByteArrayInputStream input = new DelayingByteArrayInputStream(clock); private PrintStream orgStdout; private PrintStream orgStderr; @@ -61,6 +66,7 @@ class ShellCommandTest { @BeforeEach void beforeEach() { + RegistryToolEnvironment.UNITTEST.setup(systemPropertyExtension); orgStdout = System.out; orgStderr = System.err; } @@ -82,27 +88,20 @@ class ShellCommandTest { private ShellCommand createShellCommand( CommandRunner commandRunner, Duration delay, String... commands) throws Exception { - ArrayDeque queue = new ArrayDeque<>(ImmutableList.copyOf(commands)); - BufferedReader bufferedReader = mock(BufferedReader.class); - when(bufferedReader.readLine()) - .thenAnswer( - (x) -> { - clock.advanceBy(delay); - if (queue.isEmpty()) { - throw new IOException(); - } - return queue.poll(); - }); - return new ShellCommand(bufferedReader, clock, commandRunner); + input.setInput(commands); + input.setDelay(delay); + Terminal terminal = new DumbTerminal(input, System.out); + return new ShellCommand(terminal, clock, commandRunner); } @Test void testCommandProcessing() throws Exception { - MockCli cli = new MockCli(); + FakeCli cli = new FakeCli(); ShellCommand shellCommand = - createShellCommand(cli, Duration.ZERO, "test1 foo bar", "test2 foo bar"); + createShellCommand(cli, Duration.ZERO, "test1 foo bar", "test2 foo bar", "exit"); shellCommand.run(); assertThat(cli.calls) + // "exit" causes the shell to exit and does not call cli.run. .containsExactly( ImmutableList.of("test1", "foo", "bar"), ImmutableList.of("test2", "foo", "bar")) .inOrder(); @@ -111,7 +110,7 @@ class ShellCommandTest { @Test void testNoIdleWhenInAlpha() throws Exception { RegistryToolEnvironment.ALPHA.setup(systemPropertyExtension); - MockCli cli = new MockCli(); + FakeCli cli = new FakeCli(); ShellCommand shellCommand = createShellCommand(cli, Duration.standardDays(1), "test1 foo bar", "test2 foo bar"); shellCommand.run(); @@ -120,7 +119,7 @@ class ShellCommandTest { @Test void testNoIdleWhenInSandbox() throws Exception { RegistryToolEnvironment.SANDBOX.setup(systemPropertyExtension); - MockCli cli = new MockCli(); + FakeCli cli = new FakeCli(); ShellCommand shellCommand = createShellCommand(cli, Duration.standardDays(1), "test1 foo bar", "test2 foo bar"); shellCommand.run(); @@ -129,7 +128,7 @@ class ShellCommandTest { @Test void testIdleWhenOverHourInProduction() throws Exception { RegistryToolEnvironment.PRODUCTION.setup(systemPropertyExtension); - MockCli cli = new MockCli(); + FakeCli cli = new FakeCli(); ShellCommand shellCommand = createShellCommand(cli, Duration.standardMinutes(61), "test1 foo bar", "test2 foo bar"); RuntimeException exception = assertThrows(RuntimeException.class, shellCommand::run); @@ -139,7 +138,7 @@ class ShellCommandTest { @Test void testNoIdleWhenUnderHourInProduction() throws Exception { RegistryToolEnvironment.PRODUCTION.setup(systemPropertyExtension); - MockCli cli = new MockCli(); + FakeCli cli = new FakeCli(); ShellCommand shellCommand = createShellCommand(cli, Duration.standardMinutes(59), "test1 foo bar", "test2 foo bar"); shellCommand.run(); @@ -149,7 +148,6 @@ class ShellCommandTest { void testMultipleCommandInvocations() throws Exception { RegistryCli cli = new RegistryCli("unittest", ImmutableMap.of("test_command", TestCommand.class)); - RegistryToolEnvironment.UNITTEST.setup(systemPropertyExtension); cli.setEnvironment(RegistryToolEnvironment.UNITTEST); cli.run(new String[] {"test_command", "-x", "xval", "arg1", "arg2"}); cli.run(new String[] {"test_command", "-x", "otherxval", "arg3"}); @@ -170,74 +168,72 @@ class ShellCommandTest { assertThrows(MissingCommandException.class, () -> cli.run(new String[] {"bad_command"})); } - private void performJCommanderCompletorTest( - String line, int expectedBackMotion, String... expectedCompletions) { + private void performJCommanderCompletorTest(String line, String... expectedCompletions) { JCommander jcommander = new JCommander(); + List candidates = Arrays.stream(expectedCompletions).map(Candidate::new).toList(); jcommander.setProgramName("test"); jcommander.addCommand("help", new HelpCommand(jcommander)); jcommander.addCommand("testCommand", new TestCommand()); jcommander.addCommand("testAnotherCommand", new TestAnotherCommand()); - List completions = new ArrayList<>(); - assertThat( - line.length() - - new JCommanderCompletor(jcommander) - .completeInternal(line, line.length(), completions)) - .isEqualTo(expectedBackMotion); - assertThat(completions).containsExactlyElementsIn(expectedCompletions); + List completions = new ArrayList<>(); + new JCommanderCompleter(jcommander) + .complete( + LineReaderBuilder.builder().build(), + new DefaultParser().parse(line, line.length()), + completions); + assertThat(completions).containsExactlyElementsIn(candidates); } @Test void testCompletion_commands() { - performJCommanderCompletorTest("", 0, "testCommand ", "testAnotherCommand ", "help "); - performJCommanderCompletorTest("n", 1); - performJCommanderCompletorTest("test", 4, "testCommand ", "testAnotherCommand "); - performJCommanderCompletorTest(" test", 4, "testCommand ", "testAnotherCommand "); - performJCommanderCompletorTest("testC", 5, "testCommand "); - performJCommanderCompletorTest("testA", 5, "testAnotherCommand "); + performJCommanderCompletorTest("", "testCommand ", "testAnotherCommand ", "help "); + performJCommanderCompletorTest("n"); + performJCommanderCompletorTest("test", "testCommand ", "testAnotherCommand "); + performJCommanderCompletorTest(" test", "testCommand ", "testAnotherCommand "); + performJCommanderCompletorTest("testC", "testCommand "); + performJCommanderCompletorTest("testA", "testAnotherCommand "); } @Test void testCompletion_help() { - performJCommanderCompletorTest("h", 1, "help "); - performJCommanderCompletorTest("help ", 0, "testCommand ", "testAnotherCommand ", "help "); - performJCommanderCompletorTest("help testC", 5, "testCommand "); - performJCommanderCompletorTest("help testCommand ", 0); + performJCommanderCompletorTest("h", "help "); + performJCommanderCompletorTest("help ", "testCommand ", "testAnotherCommand ", "help "); + performJCommanderCompletorTest("help testC", "testCommand "); + performJCommanderCompletorTest("help testCommand "); } @Test void testCompletion_documentation() { performJCommanderCompletorTest( "testCommand ", - 0, "", - "Main parameter: normal argument\n (java.util.List)"); - performJCommanderCompletorTest("testAnotherCommand ", 0, "", "Main parameter: [None]"); + "Main parameter: normal arguments\n (java.util.List)"); + performJCommanderCompletorTest("testAnotherCommand ", "", "Main parameter: [None]"); performJCommanderCompletorTest( - "testCommand -x ", 0, "", "Flag documentation: test parameter\n (java.lang.String)"); + "testCommand -x ", "", "Flag documentation: test parameter\n (java.lang.String)"); performJCommanderCompletorTest( - "testAnotherCommand -x ", 0, "", "Flag documentation: [No documentation available]"); + "testAnotherCommand -x ", "", "Flag documentation: [No documentation available]"); performJCommanderCompletorTest( "testCommand x ", - 0, "", - "Main parameter: normal argument\n (java.util.List)"); - performJCommanderCompletorTest("testAnotherCommand x ", 0, "", "Main parameter: [None]"); + "Main parameter: normal arguments\n (java.util.List)"); + performJCommanderCompletorTest("testAnotherCommand x ", "", "Main parameter: [None]"); } @Test void testCompletion_arguments() { - performJCommanderCompletorTest("testCommand -", 1, "-x ", "--xparam ", "--xorg "); - performJCommanderCompletorTest("testCommand --wrong", 7); - performJCommanderCompletorTest("testCommand noise --", 2, "--xparam ", "--xorg "); - performJCommanderCompletorTest("testAnotherCommand --o", 3); + performJCommanderCompletorTest("testCommand -", "-x ", "--xparam ", "--xorg "); + performJCommanderCompletorTest("testCommand --wrong"); + performJCommanderCompletorTest("testCommand noise --", "--xparam ", "--xorg "); + performJCommanderCompletorTest("testAnotherCommand --o"); } @Test void testCompletion_enum() { - performJCommanderCompletorTest("testCommand --xorg P", 1, "PRIVATE ", "PUBLIC "); - performJCommanderCompletorTest("testCommand --xorg PU", 2, "PUBLIC "); + performJCommanderCompletorTest("testCommand --xorg P", "PRIVATE ", "PUBLIC "); + performJCommanderCompletorTest("testCommand --xorg PU", "PUBLIC "); performJCommanderCompletorTest( - "testCommand --xorg ", 0, "", "Flag documentation: test organization\n (PRIVATE, PUBLIC)"); + "testCommand --xorg ", "", "Flag documentation: test organization\n (PRIVATE, PUBLIC)"); } @Test @@ -248,7 +244,7 @@ class ShellCommandTest { out.println("first line"); out.print("second line\ntrailing data"); } - assertThat(backing.toString(UTF_8)) + assertThat(backing.toString(US_ASCII)) .isEqualTo("out: first line\nout: second line\nout: trailing data\n"); } @@ -256,27 +252,28 @@ class ShellCommandTest { void testEncapsulatedOutputStream_emptyStream() throws Exception { ByteArrayOutputStream backing = new ByteArrayOutputStream(); new PrintStream(new ShellCommand.EncapsulatingOutputStream(backing, "out: ")).close(); - assertThat(backing.toString(UTF_8)).isEqualTo(""); + assertThat(backing.toString(US_ASCII)).isEqualTo(""); } @Test void testEncapsulatedOutput_command() throws Exception { - RegistryToolEnvironment.ALPHA.setup(systemPropertyExtension); captureOutput(); ShellCommand shellCommand = - new ShellCommand( + createShellCommand( args -> { System.out.println("first line"); System.err.println("second line"); System.out.print("fragmented "); System.err.println("surprise!"); System.out.println("line"); - }); + }, + Duration.ZERO, + "command1"); shellCommand.encapsulateOutput = true; shellCommand.run(); - assertThat(stderr.toString(UTF_8)).isEmpty(); - assertThat(stdout.toString(UTF_8)) + assertThat(stderr.toString(US_ASCII)).isEmpty(); + assertThat(stdout.toString(US_ASCII)) .isEqualTo( """ RUNNING "command1" @@ -290,18 +287,19 @@ class ShellCommandTest { @Test void testEncapsulatedOutput_throws() throws Exception { - RegistryToolEnvironment.ALPHA.setup(systemPropertyExtension); captureOutput(); ShellCommand shellCommand = - new ShellCommand( + createShellCommand( args -> { System.out.println("first line"); throw new Exception("some error!"); - }); + }, + Duration.ZERO, + "command1"); shellCommand.encapsulateOutput = true; shellCommand.run(); - assertThat(stderr.toString(UTF_8)).isEmpty(); - assertThat(stdout.toString(UTF_8)) + assertThat(stderr.toString(US_ASCII)).isEmpty(); + assertThat(stdout.toString(US_ASCII)) .isEqualTo( """ RUNNING "command1" @@ -318,8 +316,8 @@ class ShellCommandTest { args -> System.out.println("first line"), Duration.ZERO, "", "do something"); shellCommand.encapsulateOutput = true; shellCommand.run(); - assertThat(stderr.toString(UTF_8)).isEmpty(); - assertThat(stdout.toString(UTF_8)) + assertThat(stderr.toString(US_ASCII)).isEmpty(); + assertThat(stdout.toString(US_ASCII)) .isEqualTo("RUNNING \"do\" \"something\"\nout: first line\nSUCCESS\n"); } @@ -329,10 +327,10 @@ class ShellCommandTest { stderr = new ByteArrayOutputStream(); System.setOut(new PrintStream(stdout)); System.setErr(new PrintStream(stderr)); - System.setIn(new ByteArrayInputStream("command1\n".getBytes(UTF_8))); + System.setIn(new ByteArrayInputStream("command1\n".getBytes(US_ASCII))); } - static class MockCli implements CommandRunner { + static class FakeCli implements CommandRunner { public ArrayList> calls = new ArrayList<>(); @Override @@ -343,10 +341,12 @@ class ShellCommandTest { @Parameters(commandDescription = "Test command") static class TestCommand implements Command { - // List for recording command invocations by run(). - // - // This has to be static because it gets populated by multiple TestCommand instances, which are - // created in RegistryCli by using reflection to call the constructor. + /** + * List for recording command invocations by {@link #run}. + * + *

This has to be static because it gets populated by multiple TestCommand instances, which + * are created in {@link RegistryCli} by using reflection to call the constructor. + */ static final List> commandInvocations = new ArrayList<>(); @Parameter( @@ -358,7 +358,8 @@ class ShellCommandTest { names = {"--xorg"}, description = "test organization") OrgType orgType = OrgType.PRIVATE; - @Parameter(description = "normal argument") + + @Parameter(description = "normal arguments") List args; TestCommand() {} @@ -384,4 +385,33 @@ class ShellCommandTest { @Override public void run() {} } + + @SuppressWarnings("InputStreamSlowMultibyteRead") + private static class DelayingByteArrayInputStream extends InputStream { + private final FakeClock clock; + private ByteArrayInputStream stream; + private Duration delay; + + DelayingByteArrayInputStream(FakeClock clock) { + this.clock = clock; + } + + void setInput(String... commands) { + this.stream = + new ByteArrayInputStream((String.join("\n", commands) + "\n").getBytes(US_ASCII)); + } + + void setDelay(Duration delay) { + this.delay = delay; + } + + @Override + public int read() throws IOException { + int nextByte = stream.read(); + if (nextByte == '\n') { + clock.advanceBy(delay); + } + return nextByte; + } + } } diff --git a/dependencies.gradle b/dependencies.gradle index 461dfdcb5..8711d22b8 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -45,8 +45,6 @@ ext { 'com.google.apis:google-api-services-dns:v2beta1-rev99-1.25.0', // TODO: Migrate to v3 'com.google.apis:google-api-services-drive:v2-rev393-1.25.0', - // TODO: Migrate to org.jline - 'jline:jline:1.0', // TODO: Remove after the legacy console is deleted. 'com.google.closure-stylesheets:closure-stylesheets:1.5.0', @@ -220,6 +218,7 @@ ext { 'org.hamcrest:hamcrest-library:[2.2,)', 'org.hamcrest:hamcrest:[2.2,)', 'org.jcommander:jcommander:[1.83,)', + 'org.jline:jline:[3.0,)', 'org.joda:joda-money:[1.0.1,)', 'org.json:json:[20160810,)', 'org.jsoup:jsoup:[1.13.1,)', diff --git a/jetty/gradle.lockfile b/jetty/gradle.lockfile index 74022b563..201d9bdc9 100644 --- a/jetty/gradle.lockfile +++ b/jetty/gradle.lockfile @@ -271,7 +271,6 @@ javax.persistence:javax.persistence-api:2.2=deploy_jar,runtimeClasspath,testRunt javax.servlet:servlet-api:2.5=deploy_jar,runtimeClasspath,testRuntimeClasspath javax.validation:validation-api:1.0.0.GA=deploy_jar,runtimeClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=deploy_jar,runtimeClasspath,testRuntimeClasspath -jline:jline:1.0=deploy_jar,runtimeClasspath,testRuntimeClasspath joda-time:joda-time:2.10.14=deploy_jar,runtimeClasspath,testRuntimeClasspath junit:junit:4.13.2=testRuntimeClasspath net.bytebuddy:byte-buddy:1.12.18=deploy_jar,runtimeClasspath,testRuntimeClasspath @@ -351,6 +350,7 @@ org.jetbrains.kotlinx:kotlinx-datetime:0.4.0=deploy_jar,runtimeClasspath,testRun org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.0.1=deploy_jar,runtimeClasspath,testRuntimeClasspath org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.1=deploy_jar,runtimeClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath +org.jline:jline:3.25.1=deploy_jar,runtimeClasspath,testRuntimeClasspath org.joda:joda-money:1.0.4=deploy_jar,runtimeClasspath,testRuntimeClasspath org.json:json:20231013=deploy_jar,runtimeClasspath,testRuntimeClasspath org.jsoup:jsoup:1.17.2=deploy_jar,runtimeClasspath,testRuntimeClasspath diff --git a/services/backend/gradle.lockfile b/services/backend/gradle.lockfile index a45f88bc2..317fa14f1 100644 --- a/services/backend/gradle.lockfile +++ b/services/backend/gradle.lockfile @@ -253,7 +253,6 @@ javax.persistence:javax.persistence-api:2.2=compileClasspath,runtimeClasspath,te javax.servlet:servlet-api:2.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.validation:validation-api:1.0.0.GA=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jline:jline:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath joda-time:joda-time:2.10.14=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.12.18=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.java.dev.jna:jna:5.13.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -323,6 +322,7 @@ org.jetbrains.kotlinx:kotlinx-datetime:0.4.0=compileClasspath,runtimeClasspath,t org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.0.1=runtimeClasspath,testRuntimeClasspath org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.1=runtimeClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jline:jline:3.25.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.joda:joda-money:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.json:json:20231013=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jsoup:jsoup:1.17.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/services/bsa/gradle.lockfile b/services/bsa/gradle.lockfile index a45f88bc2..317fa14f1 100644 --- a/services/bsa/gradle.lockfile +++ b/services/bsa/gradle.lockfile @@ -253,7 +253,6 @@ javax.persistence:javax.persistence-api:2.2=compileClasspath,runtimeClasspath,te javax.servlet:servlet-api:2.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.validation:validation-api:1.0.0.GA=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jline:jline:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath joda-time:joda-time:2.10.14=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.12.18=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.java.dev.jna:jna:5.13.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -323,6 +322,7 @@ org.jetbrains.kotlinx:kotlinx-datetime:0.4.0=compileClasspath,runtimeClasspath,t org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.0.1=runtimeClasspath,testRuntimeClasspath org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.1=runtimeClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jline:jline:3.25.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.joda:joda-money:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.json:json:20231013=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jsoup:jsoup:1.17.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/services/default/gradle.lockfile b/services/default/gradle.lockfile index a45f88bc2..317fa14f1 100644 --- a/services/default/gradle.lockfile +++ b/services/default/gradle.lockfile @@ -253,7 +253,6 @@ javax.persistence:javax.persistence-api:2.2=compileClasspath,runtimeClasspath,te javax.servlet:servlet-api:2.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.validation:validation-api:1.0.0.GA=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jline:jline:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath joda-time:joda-time:2.10.14=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.12.18=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.java.dev.jna:jna:5.13.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -323,6 +322,7 @@ org.jetbrains.kotlinx:kotlinx-datetime:0.4.0=compileClasspath,runtimeClasspath,t org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.0.1=runtimeClasspath,testRuntimeClasspath org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.1=runtimeClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jline:jline:3.25.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.joda:joda-money:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.json:json:20231013=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jsoup:jsoup:1.17.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/services/pubapi/gradle.lockfile b/services/pubapi/gradle.lockfile index a45f88bc2..317fa14f1 100644 --- a/services/pubapi/gradle.lockfile +++ b/services/pubapi/gradle.lockfile @@ -253,7 +253,6 @@ javax.persistence:javax.persistence-api:2.2=compileClasspath,runtimeClasspath,te javax.servlet:servlet-api:2.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.validation:validation-api:1.0.0.GA=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jline:jline:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath joda-time:joda-time:2.10.14=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.12.18=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.java.dev.jna:jna:5.13.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -323,6 +322,7 @@ org.jetbrains.kotlinx:kotlinx-datetime:0.4.0=compileClasspath,runtimeClasspath,t org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.0.1=runtimeClasspath,testRuntimeClasspath org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.1=runtimeClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jline:jline:3.25.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.joda:joda-money:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.json:json:20231013=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jsoup:jsoup:1.17.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/services/tools/gradle.lockfile b/services/tools/gradle.lockfile index a45f88bc2..317fa14f1 100644 --- a/services/tools/gradle.lockfile +++ b/services/tools/gradle.lockfile @@ -253,7 +253,6 @@ javax.persistence:javax.persistence-api:2.2=compileClasspath,runtimeClasspath,te javax.servlet:servlet-api:2.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.validation:validation-api:1.0.0.GA=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jline:jline:1.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath joda-time:joda-time:2.10.14=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.12.18=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.java.dev.jna:jna:5.13.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -323,6 +322,7 @@ org.jetbrains.kotlinx:kotlinx-datetime:0.4.0=compileClasspath,runtimeClasspath,t org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.0.1=runtimeClasspath,testRuntimeClasspath org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.1=runtimeClasspath,testRuntimeClasspath org.jetbrains:annotations:17.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jline:jline:3.25.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.joda:joda-money:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.json:json:20231013=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.jsoup:jsoup:1.17.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath