diff --git a/cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java b/cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java index 400b42d9..7b7fb6be 100644 --- a/cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java +++ b/cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java @@ -4,6 +4,7 @@ import org.neo4j.shell.cli.CliArgs; import org.neo4j.shell.log.AnsiLogger; import org.neo4j.shell.log.Logger; +import org.neo4j.shell.prettyprint.PrettyConfig; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -32,8 +33,7 @@ public void connectInteractivelyPromptsOnWrongAuthentication() throws Exception cliArgs.setPassword( "", "" ); Logger logger = new AnsiLogger(cliArgs.getDebugMode()); - logger.setFormat(cliArgs.getFormat()); - + PrettyConfig prettyConfig = new PrettyConfig(cliArgs); ConnectionConfig connectionConfig = new ConnectionConfig( cliArgs.getScheme(), cliArgs.getHost(), @@ -42,7 +42,7 @@ public void connectInteractivelyPromptsOnWrongAuthentication() throws Exception cliArgs.getPassword(), cliArgs.getEncryption()); - CypherShell shell = new CypherShell(logger); + CypherShell shell = new CypherShell(logger, prettyConfig); // when assertEquals("", connectionConfig.username()); diff --git a/cypher-shell/src/integration-test/java/org/neo4j/shell/StringLinePrinter.java b/cypher-shell/src/integration-test/java/org/neo4j/shell/StringLinePrinter.java new file mode 100644 index 00000000..ed03598b --- /dev/null +++ b/cypher-shell/src/integration-test/java/org/neo4j/shell/StringLinePrinter.java @@ -0,0 +1,22 @@ +package org.neo4j.shell; + +import org.neo4j.shell.prettyprint.LinePrinter; +import org.neo4j.shell.prettyprint.OutputFormatter; + +public class StringLinePrinter implements LinePrinter { + + private StringBuilder sb = new StringBuilder(); + + @Override + public void printOut(String line) { + sb.append(line).append(OutputFormatter.NEWLINE); + } + + public void clear() { + sb.setLength(0); + } + + public String output() { + return sb.toString(); + } +} diff --git a/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellFailureIntegrationTest.java b/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellFailureIntegrationTest.java index e3af4b80..2519f573 100644 --- a/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellFailureIntegrationTest.java +++ b/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellFailureIntegrationTest.java @@ -5,29 +5,25 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; - import org.neo4j.driver.v1.exceptions.AuthenticationException; import org.neo4j.shell.ConnectionConfig; import org.neo4j.shell.CypherShell; +import org.neo4j.shell.StringLinePrinter; import org.neo4j.shell.cli.Format; import org.neo4j.shell.exception.CommandException; -import org.neo4j.shell.log.Logger; - -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; +import org.neo4j.shell.prettyprint.PrettyConfig; public class CypherShellFailureIntegrationTest { @Rule public final ExpectedException thrown = ExpectedException.none(); - private Logger logger = mock(Logger.class); + private StringLinePrinter linePrinter = new StringLinePrinter(); private CypherShell shell; @Before public void setUp() throws Exception { - doReturn(Format.VERBOSE).when(logger).getFormat(); - - shell = new CypherShell(logger); + linePrinter.clear(); + shell = new CypherShell(linePrinter, new PrettyConfig(Format.VERBOSE, true, 1000)); } @Test diff --git a/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellPlainIntegrationTest.java b/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellPlainIntegrationTest.java index 6ba76f68..40347fd8 100644 --- a/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellPlainIntegrationTest.java +++ b/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellPlainIntegrationTest.java @@ -6,33 +6,28 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.mockito.ArgumentCaptor; import org.neo4j.shell.ConnectionConfig; import org.neo4j.shell.CypherShell; +import org.neo4j.shell.StringLinePrinter; import org.neo4j.shell.cli.Format; import org.neo4j.shell.exception.CommandException; -import org.neo4j.shell.log.Logger; - -import java.util.List; +import org.neo4j.shell.prettyprint.PrettyConfig; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.neo4j.shell.prettyprint.OutputFormatter.NEWLINE; public class CypherShellPlainIntegrationTest { @Rule public final ExpectedException thrown = ExpectedException.none(); - private Logger logger = mock(Logger.class); + private StringLinePrinter linePrinter = new StringLinePrinter(); private CypherShell shell; @Before public void setUp() throws Exception { - doReturn(Format.PLAIN).when(logger).getFormat(); - shell = new CypherShell(logger); + linePrinter.clear(); + shell = new CypherShell(linePrinter, new PrettyConfig(Format.PLAIN, true, 1000)); shell.connect(new ConnectionConfig("bolt://", "localhost", 7687, "neo4j", "neo", true)); } @@ -46,14 +41,11 @@ public void periodicCommitWorks() throws CommandException { shell.execute("USING PERIODIC COMMIT\n" + "LOAD CSV FROM 'https://neo4j.com/docs/cypher-refcard/3.2/csv/artists.csv' AS line\n" + "CREATE (:Artist {name: line[1], year: toInt(line[2])});"); + linePrinter.clear(); shell.execute("MATCH (a:Artist) WHERE a.name = 'Europe' RETURN a.name"); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, times(2)).printOut(captor.capture()); - - List queryResult = captor.getAllValues(); - assertThat(queryResult.get(1), containsString("Europe")); + assertThat(linePrinter.output(), containsString("a.name"+ NEWLINE+"\"Europe\"")); } @Test @@ -62,11 +54,7 @@ public void cypherWithProfileStatements() throws CommandException { shell.execute("CYPHER RUNTIME=INTERPRETED PROFILE RETURN null"); //then - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, times(1)).printOut(captor.capture()); - - List result = captor.getAllValues(); - String actual = result.get(0); + String actual = linePrinter.output(); // This assertion checks everything except for time and cypher assertThat(actual, containsString("Plan: \"PROFILE\"")); assertThat(actual, containsString("Statement: \"READ_ONLY\"")); @@ -84,11 +72,7 @@ public void cypherWithExplainStatements() throws CommandException { shell.execute("CYPHER RUNTIME=INTERPRETED EXPLAIN RETURN null"); //then - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, times(1)).printOut(captor.capture()); - - List result = captor.getAllValues(); - String actual = result.get(0); + String actual = linePrinter.output(); // This assertion checks everything except for time and cypher assertThat(actual, containsString("Plan: \"EXPLAIN\"")); assertThat(actual, containsString("Statement: \"READ_ONLY\"")); diff --git a/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellVerboseIntegrationTest.java b/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellVerboseIntegrationTest.java index f3888db2..d9c9614b 100644 --- a/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellVerboseIntegrationTest.java +++ b/cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellVerboseIntegrationTest.java @@ -5,29 +5,28 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.mockito.ArgumentCaptor; import org.neo4j.shell.ConnectionConfig; import org.neo4j.shell.CypherShell; +import org.neo4j.shell.StringLinePrinter; import org.neo4j.shell.cli.Format; import org.neo4j.shell.exception.CommandException; -import org.neo4j.shell.log.Logger; +import org.neo4j.shell.prettyprint.PrettyConfig; -import java.util.List; import java.util.Optional; -import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; -import static org.mockito.Mockito.*; import static org.neo4j.shell.Versions.majorVersion; +import static org.neo4j.shell.Versions.minorVersion; public class CypherShellVerboseIntegrationTest { @Rule public final ExpectedException thrown = ExpectedException.none(); - private Logger logger = mock(Logger.class); + private StringLinePrinter linePrinter = new StringLinePrinter(); private Command rollbackCommand; private Command commitCommand; private Command beginCommand; @@ -35,9 +34,8 @@ public class CypherShellVerboseIntegrationTest { @Before public void setUp() throws Exception { - doReturn(Format.VERBOSE).when(logger).getFormat(); - - shell = new CypherShell(logger); + linePrinter.clear(); + shell = new CypherShell(linePrinter, new PrettyConfig(Format.VERBOSE, true, 1000)); rollbackCommand = new Rollback(shell); commitCommand = new Commit(shell); beginCommand = new Begin(shell); @@ -56,11 +54,7 @@ public void cypherWithNoReturnStatements() throws CommandException { shell.execute("CREATE (:TestPerson {name: \"Jane Smith\"})"); //then - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, times(1)).printOut(captor.capture()); - - List result = captor.getAllValues(); - assertThat(result.get(0), containsString("Added 1 nodes, Set 1 properties, Added 1 labels")); + assertThat(linePrinter.output(), containsString("Added 1 nodes, Set 1 properties, Added 1 labels")); } @Test @@ -69,13 +63,10 @@ public void cypherWithReturnStatements() throws CommandException { shell.execute("CREATE (jane :TestPerson {name: \"Jane Smith\"}) RETURN jane"); //then - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, times(1)).printOut(captor.capture()); - - List result = captor.getAllValues(); - assertThat(result.get(0), containsString("| jane ")); - assertThat(result.get(0), containsString("| (:TestPerson {name: \"Jane Smith\"}) |" )); - assertThat(result.get(0), containsString("Added 1 nodes, Set 1 properties, Added 1 labels")); + String output = linePrinter.output(); + assertThat(output, containsString("| jane ")); + assertThat(output, containsString("| (:TestPerson {name: \"Jane Smith\"}) |" )); + assertThat(output, containsString("Added 1 nodes, Set 1 properties, Added 1 labels")); } @Test @@ -95,18 +86,16 @@ public void rollbackScenario() throws CommandException { //when beginCommand.execute(""); - shell.execute("CREATE (:Random)"); + shell.execute("CREATE (:NotCreated)"); rollbackCommand.execute(""); //then - shell.execute("MATCH (n:TestPerson) RETURN n ORDER BY n.name"); - - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, times(2)).printOut(captor.capture()); + shell.execute("MATCH (n) RETURN n"); - List result = captor.getAllValues(); - assertThat(result.get(1), containsString("| n ")); - assertThat(result.get(1), containsString("| (:TestPerson {name: \"Jane Smith\"}) |")); + String output = linePrinter.output(); + assertThat(output, containsString("| n ")); + assertThat(output, containsString("| (:TestPerson {name: \"Jane Smith\"}) |")); + assertThat(output, not(containsString(":NotCreated"))); } @Test @@ -119,47 +108,48 @@ public void resetOutOfTxScenario() throws CommandException { shell.execute("CREATE (:TestPerson {name: \"Jane Smith\"})"); shell.execute("MATCH (n:TestPerson) RETURN n ORDER BY n.name"); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, times(3)).printOut(captor.capture()); - - List result = captor.getAllValues(); - assertThat(result.get(2), containsString("| (:TestPerson {name: \"Jane Smith\"}) |" + - "\n| (:TestPerson {name: \"Jane Smith\"}) |")); + String result = linePrinter.output(); + assertThat(result, containsString( + "| (:TestPerson {name: \"Jane Smith\"}) |\n" + + "| (:TestPerson {name: \"Jane Smith\"}) |")); } @Test public void resetInTxScenario() throws CommandException { //when beginCommand.execute(""); - shell.execute("CREATE (:Random)"); + shell.execute("CREATE (:NotCreated)"); shell.reset(); //then shell.execute("CREATE (:TestPerson {name: \"Jane Smith\"})"); - shell.execute("MATCH (n:TestPerson) RETURN n ORDER BY n.name"); + shell.execute("MATCH (n) RETURN n"); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, times(2)).printOut(captor.capture()); - - List result = captor.getAllValues(); - assertThat(result.get(1), containsString("| (:TestPerson {name: \"Jane Smith\"}) |")); + String result = linePrinter.output(); + assertThat(result, containsString("| (:TestPerson {name: \"Jane Smith\"}) |")); + assertThat(result, not(containsString(":NotCreated"))); } @Test public void commitScenario() throws CommandException { beginCommand.execute(""); shell.execute("CREATE (:TestPerson {name: \"Joe Smith\"})"); + assertThat(linePrinter.output(), equalTo("")); + // Here ^^ we assert that nothing is printed before commit on explicit transactions. This was + // existing behaviour on typing this comment, but it could we worth thinking that through if + // we change explicit transaction queries to stream results. + shell.execute("CREATE (:TestPerson {name: \"Jane Smith\"})"); + assertThat(linePrinter.output(), equalTo("")); + shell.execute("MATCH (n:TestPerson) RETURN n ORDER BY n.name"); + assertThat(linePrinter.output(), equalTo("")); + commitCommand.execute(""); //then - - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, times(3)).printOut(captor.capture()); - - List result = captor.getAllValues(); - assertThat(result.get(2), + String result = linePrinter.output(); + assertThat(result, containsString("\n| (:TestPerson {name: \"Jane Smith\"}) |\n| (:TestPerson {name: \"Joe Smith\"}) |\n")); } @@ -170,18 +160,16 @@ public void paramsAndListVariables() throws CommandException { long randomLong = System.currentTimeMillis(); String stringInput = "\"randomString\""; shell.setParameter("string", stringInput); - Optional result = shell.setParameter("bob", String.valueOf(randomLong)); - assertTrue(result.isPresent()); - assertEquals(randomLong, result.get()); - shell.execute("RETURN { bob }, $string"); + Optional bob = shell.setParameter("bob", String.valueOf(randomLong)); + assertTrue(bob.isPresent()); + assertEquals(randomLong, bob.get()); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger).printOut(captor.capture()); + shell.execute("RETURN { bob }, $string"); - List queryResult = captor.getAllValues(); - assertThat(queryResult.get(0), containsString("| { bob }")); - assertThat(queryResult.get(0), containsString("| " + randomLong + " | " + stringInput + " |")); + String result = linePrinter.output(); + assertThat(result, containsString("| { bob }")); + assertThat(result, containsString("| " + randomLong + " | " + stringInput + " |")); assertEquals(randomLong, shell.allParameterValues().get("bob")); assertEquals("randomString", shell.allParameterValues().get("string")); } @@ -191,40 +179,33 @@ public void paramsAndListVariablesWithSpecialCharacters() throws CommandExceptio assertTrue(shell.allParameterValues().isEmpty()); long randomLong = System.currentTimeMillis(); - Optional result = shell.setParameter("`bob`", String.valueOf(randomLong)); - assertTrue(result.isPresent()); - assertEquals(randomLong, result.get()); + Optional bob = shell.setParameter("`bob`", String.valueOf(randomLong)); + assertTrue(bob.isPresent()); + assertEquals(randomLong, bob.get()); shell.execute("RETURN { `bob` }"); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger).printOut(captor.capture()); - - List queryResult = captor.getAllValues(); - assertThat(queryResult.get(0), containsString("| { `bob` }")); - assertThat(queryResult.get(0), containsString("\n| " + randomLong+ " |\n")); + String result = linePrinter.output(); + assertThat(result, containsString("| { `bob` }")); + assertThat(result, containsString("\n| " + randomLong+ " |\n")); assertEquals(randomLong, shell.allParameterValues().get("bob")); } @Test public void cypherWithOrder() throws CommandException { // given + String serverVersion = shell.getServerVersion(); + assumeTrue(minorVersion(serverVersion) == 6 || majorVersion(serverVersion) == 4); + shell.execute( "CREATE INDEX ON :Person(age)" ); //when shell.execute("CYPHER RUNTIME=INTERPRETED EXPLAIN MATCH (n:Person) WHERE n.age >= 18 RETURN n.name, n.age"); //then - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, times(2)).printOut(captor.capture()); - - List result = captor.getAllValues(); - String actual = result.get(0); - if ( actual.contains( "CYPHER 3.5" )) // Sadly best way to have test that relies on 3.5 functionality... - { - assertThat( actual, containsString( "Ordered by" ) ); - assertThat( actual, containsString( "n.age ASC" ) ); - } + String actual = linePrinter.output(); + assertThat( actual, containsString( "Ordered by" ) ); + assertThat( actual, containsString( "n.age ASC" ) ); } @Test @@ -236,11 +217,7 @@ public void cypherWithExplainAndRulePlanner() throws CommandException { shell.execute("CYPHER planner=rule EXPLAIN MATCH (e:E) WHERE e.bucket='Live' and e.id = 23253473 RETURN count(e)"); //then - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, times(1)).printOut(captor.capture()); - - List result = captor.getAllValues(); - String actual = result.get(0); + String actual = linePrinter.output(); assertThat(actual, containsString("\"EXPLAIN\"")); assertThat(actual, containsString("\"READ_ONLY\"")); assertThat(actual, containsString("\"RULE\"")); diff --git a/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java b/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java index 8c662637..8fe6e69c 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java @@ -1,12 +1,14 @@ package org.neo4j.shell; +import org.neo4j.driver.v1.Record; import org.neo4j.shell.commands.Command; import org.neo4j.shell.commands.CommandExecutable; import org.neo4j.shell.commands.CommandHelper; import org.neo4j.shell.exception.CommandException; import org.neo4j.shell.exception.ExitException; -import org.neo4j.shell.log.Logger; import org.neo4j.shell.prettyprint.CypherVariablesFormatter; +import org.neo4j.shell.prettyprint.LinePrinter; +import org.neo4j.shell.prettyprint.PrettyConfig; import org.neo4j.shell.prettyprint.PrettyPrinter; import org.neo4j.shell.state.BoltResult; import org.neo4j.shell.state.BoltStateHandler; @@ -28,19 +30,19 @@ public class CypherShell implements StatementExecuter, Connector, TransactionHan // Final space to catch newline protected static final Pattern cmdNamePattern = Pattern.compile("^\\s*(?[^\\s]+)\\b(?.*)\\s*$"); protected final Map queryParams = new HashMap<>(); - private final Logger logger; + private final LinePrinter linePrinter; private final BoltStateHandler boltStateHandler; private final PrettyPrinter prettyPrinter; protected CommandHelper commandHelper; - public CypherShell(@Nonnull Logger logger) { - this(logger, new BoltStateHandler(), new PrettyPrinter(logger.getFormat())); + public CypherShell(@Nonnull LinePrinter linePrinter, @Nonnull PrettyConfig prettyConfig) { + this(linePrinter, new BoltStateHandler(), new PrettyPrinter(prettyConfig)); } - protected CypherShell(@Nonnull Logger logger, + protected CypherShell(@Nonnull LinePrinter linePrinter, @Nonnull BoltStateHandler boltStateHandler, @Nonnull PrettyPrinter prettyPrinter) { - this.logger = logger; + this.linePrinter = linePrinter; this.boltStateHandler = boltStateHandler; this.prettyPrinter = prettyPrinter; addRuntimeHookToResetShell(); @@ -83,7 +85,7 @@ public void execute(@Nonnull final String cmdString) throws ExitException, Comma */ protected void executeCypher(@Nonnull final String cypher) throws CommandException { final Optional result = boltStateHandler.runCypher(cypher, allParameterValues()); - result.ifPresent(boltResult -> logger.printOut(prettyPrinter.format(boltResult))); + result.ifPresent(boltResult -> prettyPrinter.format(boltResult, linePrinter)); } @Override @@ -138,7 +140,7 @@ public void beginTransaction() throws CommandException { @Override public Optional> commitTransaction() throws CommandException { Optional> results = boltStateHandler.commitTransaction(); - results.ifPresent(boltResult -> boltResult.forEach(result -> logger.printOut(prettyPrinter.format(result)))); + results.ifPresent(boltResult -> boltResult.forEach(result -> prettyPrinter.format(result, linePrinter))); return results; } @@ -154,21 +156,25 @@ public boolean isTransactionOpen() { @Override @Nonnull - public Optional setParameter(@Nonnull String name, @Nonnull String valueString) throws CommandException { - final BoltResult result = setParamsAndValidate(name, valueString); + public Optional setParameter(@Nonnull String name, @Nonnull String valueString) throws CommandException { + final Record records = evaluateParamOnServer(name, valueString); String parameterName = CypherVariablesFormatter.unescapedCypherVariable(name); - final Object value = result.getRecords().get(0).get(parameterName).asObject(); + final Object value = records.get(parameterName).asObject(); queryParams.put(parameterName, new ParamValue(valueString, value)); return Optional.ofNullable(value); } - private BoltResult setParamsAndValidate(@Nonnull String name, @Nonnull String valueString) throws CommandException { + private Record evaluateParamOnServer(@Nonnull String name, @Nonnull String valueString) throws CommandException { String cypher = "RETURN " + valueString + " as " + name; - final Optional result = boltStateHandler.runCypher(cypher, allParameterValues()); - if (!result.isPresent() || result.get().getRecords().isEmpty()) { + final Optional resultOpt = boltStateHandler.runCypher(cypher, allParameterValues()); + if (!resultOpt.isPresent()) { + throw new CommandException("Failed to set value of parameter"); + } + List records = resultOpt.get().getRecords(); + if (records.size() != 1) { throw new CommandException("Failed to set value of parameter"); } - return result.get(); + return records.get(0); } @Override diff --git a/cypher-shell/src/main/java/org/neo4j/shell/Main.java b/cypher-shell/src/main/java/org/neo4j/shell/Main.java index 76f8e196..6d097075 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/Main.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/Main.java @@ -10,6 +10,7 @@ import org.neo4j.shell.exception.CommandException; import org.neo4j.shell.log.AnsiLogger; import org.neo4j.shell.log.Logger; +import org.neo4j.shell.prettyprint.PrettyConfig; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -58,7 +59,8 @@ void startShell(@Nonnull CliArgs cliArgs) { if (cliArgs.getVersion() || cliArgs.getDriverVersion()) { return; } - Logger logger = instantiateLogger(cliArgs); + Logger logger = new AnsiLogger(cliArgs.getDebugMode()); + PrettyConfig prettyConfig = new PrettyConfig(cliArgs); ConnectionConfig connectionConfig = new ConnectionConfig( cliArgs.getScheme(), cliArgs.getHost(), @@ -68,7 +70,7 @@ void startShell(@Nonnull CliArgs cliArgs) { cliArgs.getEncryption()); try { - CypherShell shell = new CypherShell(logger); + CypherShell shell = new CypherShell(logger, prettyConfig); // Can only prompt for password if input has not been redirected connectMaybeInteractively(shell, connectionConfig, isInputInteractive()); @@ -87,15 +89,6 @@ void startShell(@Nonnull CliArgs cliArgs) { } } - private Logger instantiateLogger(@Nonnull CliArgs cliArgs) { - Logger logger = new AnsiLogger(cliArgs.getDebugMode()); - logger.setFormat(cliArgs.getFormat()); - if (cliArgs.isStringShell() && Format.AUTO.equals(cliArgs.getFormat())) { - logger.setFormat(Format.PLAIN); - } - return logger; - } - /** * Connect the shell to the server, and try to handle missing passwords and such */ diff --git a/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgHelper.java b/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgHelper.java index 42e24f27..0d6d5adc 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgHelper.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgHelper.java @@ -5,11 +5,7 @@ import net.sourceforge.argparse4j.impl.action.StoreTrueArgumentAction; import net.sourceforge.argparse4j.impl.choice.CollectionArgumentChoice; import net.sourceforge.argparse4j.impl.type.BooleanArgumentType; -import net.sourceforge.argparse4j.inf.ArgumentGroup; -import net.sourceforge.argparse4j.inf.ArgumentParser; -import net.sourceforge.argparse4j.inf.ArgumentParserException; -import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup; -import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.*; import java.io.PrintWriter; import java.util.regex.Matcher; @@ -88,6 +84,10 @@ public static CliArgs parse(@Nonnull String... args) { cliArgs.setNonInteractive(ns.getBoolean("force-non-interactive")); + cliArgs.setWrap(ns.getBoolean("wrap")); + + cliArgs.setNumSampleRows(ns.getInt("sample-rows")); + cliArgs.setVersion(ns.getBoolean("version")); cliArgs.setDriverVersion(ns.getBoolean("driver-version")); @@ -167,6 +167,17 @@ private static ArgumentParser setupParser() .dest("force-non-interactive") .action(new StoreTrueArgumentAction()); + parser.addArgument("--sample-rows") + .help("number of rows sampled to compute table widths (only for format=VERBOSE)") + .type(new PositiveIntegerType()) + .dest("sample-rows") + .setDefault(CliArgs.DEFAULT_NUM_SAMPLE_ROWS); + + parser.addArgument("--wrap") + .help("wrap table colum values if column is too narrow (only for format=VERBOSE)") + .type(new BooleanArgumentType()) + .setDefault(true); + parser.addArgument("-v", "--version") .help("print version of cypher-shell and exit") .action(new StoreTrueArgumentAction()); @@ -183,5 +194,16 @@ private static ArgumentParser setupParser() return parser; } - + private static class PositiveIntegerType implements ArgumentType { + @Override + public Integer convert(ArgumentParser parser, Argument arg, String value) throws ArgumentParserException { + try { + int result = Integer.parseInt(value); + if (result < 1) throw new NumberFormatException(value); + return result; + } catch (NumberFormatException nfe) { + throw new ArgumentParserException("Invalid value: "+value, parser); + } + } + } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgs.java b/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgs.java index 975e311b..44f4d258 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgs.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgs.java @@ -5,9 +5,14 @@ import java.util.Optional; public class CliArgs { - private String scheme = "bolt://"; - private String host = "localhost"; - private int port = 7687; + private static final String DEFAULT_SCHEME = "bolt://"; + private static final String DEFAULT_HOST = "localhost"; + private static final int DEFAULT_PORT = 7687; + static final int DEFAULT_NUM_SAMPLE_ROWS = 1000; + + private String scheme = DEFAULT_SCHEME; + private String host = DEFAULT_HOST; + private int port = DEFAULT_PORT; private String username = ""; private String password = ""; private FailBehavior failBehavior = FailBehavior.FAIL_FAST; @@ -18,6 +23,8 @@ public class CliArgs { private boolean nonInteractive = false; private boolean version = false; private boolean driverVersion = false; + private int numSampleRows = DEFAULT_NUM_SAMPLE_ROWS; + private boolean wrap = true; /** * Set the scheme to the primary value, or if null, the fallback value. @@ -106,7 +113,6 @@ public String getHost() { return host; } - @Nonnull public int getPort() { return port; } @@ -167,4 +173,22 @@ public void setDriverVersion(boolean version) { public boolean isStringShell() { return cypher.isPresent(); } + + public boolean getWrap() { + return wrap; + } + + public void setWrap(boolean wrap) { + this.wrap = wrap; + } + + public int getNumSampleRows() { + return numSampleRows; + } + + public void setNumSampleRows(Integer numSampleRows) { + if (numSampleRows != null && numSampleRows > 0) { + this.numSampleRows = numSampleRows; + } + } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/log/Logger.java b/cypher-shell/src/main/java/org/neo4j/shell/log/Logger.java index c91c3b8e..5e319867 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/log/Logger.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/log/Logger.java @@ -1,11 +1,12 @@ package org.neo4j.shell.log; import org.neo4j.shell.cli.Format; +import org.neo4j.shell.prettyprint.LinePrinter; import javax.annotation.Nonnull; import java.io.PrintStream; -public interface Logger { +public interface Logger extends LinePrinter { /** * @return the output stream */ @@ -33,13 +34,6 @@ public interface Logger { */ void printError(@Nonnull String text); - /** - * Print the designated text to configured output stream. - * - * @param text to print to the output stream - */ - void printOut(@Nonnull String text); - /** * @return the current format of the logger */ diff --git a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/LinePrinter.java b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/LinePrinter.java new file mode 100644 index 00000000..248176c0 --- /dev/null +++ b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/LinePrinter.java @@ -0,0 +1,15 @@ +package org.neo4j.shell.prettyprint; + +/** + * Prints lines. + */ +@FunctionalInterface +public interface LinePrinter { + + /** + * Print the designated line to configured output stream. + * + * @param line to print to the output stream + */ + void printOut(String line ); +} diff --git a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/OutputFormatter.java b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/OutputFormatter.java index e8b0d5fd..6c62072c 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/OutputFormatter.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/OutputFormatter.java @@ -13,11 +13,7 @@ import org.neo4j.shell.state.BoltResult; import javax.annotation.Nonnull; -import java.util.Arrays; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import static java.util.Arrays.asList; @@ -26,15 +22,18 @@ public interface OutputFormatter { + enum Capablities {info, plan, result, footer, statistics} + String COMMA_SEPARATOR = ", "; String COLON_SEPARATOR = ": "; String COLON = ":"; String SPACE = " "; String NEWLINE = System.getProperty("line.separator"); - @Nonnull String format(@Nonnull BoltResult result); + void format(@Nonnull BoltResult result, @Nonnull LinePrinter linePrinter); - @Nonnull default String formatValue(@Nonnull final Value value) { + @Nonnull default String formatValue(final Value value) { + if (value == null) return ""; TypeRepresentation type = (TypeRepresentation) value.type(); switch (type.constructor()) { case LIST: @@ -101,7 +100,7 @@ default String pathAsString(@Nonnull Path path) { } } - return list.stream().collect(Collectors.joining()); + return String.join("", list); } @Nonnull default String relationshipAsString(@Nonnull Relationship relationship) { @@ -164,7 +163,7 @@ static boolean isNotBlank(String string) { } @Nonnull static String rightPad(@Nonnull String str, int width) { - return rightPad(str,width,' '); + return rightPad(str, width, ' '); } @Nonnull static String rightPad(@Nonnull String str, int width, char c) { int actualSize = str.length(); @@ -187,6 +186,7 @@ static boolean isNotBlank(String string) { return ""; } + Set capabilities(); List INFO = asList("Version", "Planner", "Runtime"); diff --git a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/PrettyConfig.java b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/PrettyConfig.java new file mode 100644 index 00000000..b65f1ad8 --- /dev/null +++ b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/PrettyConfig.java @@ -0,0 +1,33 @@ +package org.neo4j.shell.prettyprint; + +import org.neo4j.shell.cli.CliArgs; +import org.neo4j.shell.cli.Format; + +/** + * Configuration of pretty printer. + */ +public class PrettyConfig { + + public static final PrettyConfig DEFAULT = new PrettyConfig(new CliArgs()); + + public final Format format; + public final boolean wrap; + public final int numSampleRows; + + public PrettyConfig(CliArgs cliArgs) { + this(selectFormat(cliArgs), cliArgs.getWrap(), cliArgs.getNumSampleRows()); + } + + private static Format selectFormat(CliArgs cliArgs) { + if (cliArgs.isStringShell() && Format.AUTO.equals(cliArgs.getFormat())) { + return Format.PLAIN; + } + return cliArgs.getFormat(); + } + + public PrettyConfig(Format format, boolean wrap, int numSampleRows) { + this.format = format; + this.wrap = wrap; + this.numSampleRows = numSampleRows; + } +} diff --git a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/PrettyPrinter.java b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/PrettyPrinter.java index 17026f5d..f0febc2b 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/PrettyPrinter.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/PrettyPrinter.java @@ -5,7 +5,7 @@ import javax.annotation.Nonnull; -import static java.util.Arrays.asList; +import java.util.Set; /** * Print the result from neo4j in a intelligible fashion. @@ -14,17 +14,34 @@ public class PrettyPrinter { private final StatisticsCollector statisticsCollector; private final OutputFormatter outputFormatter; - public PrettyPrinter(@Nonnull Format format) { - this.statisticsCollector = new StatisticsCollector(format); - this.outputFormatter = format == Format.VERBOSE ? new TableOutputFormatter() : new SimpleOutputFormatter(); + public PrettyPrinter(@Nonnull PrettyConfig prettyConfig) { + this.statisticsCollector = new StatisticsCollector(prettyConfig.format); + this.outputFormatter = selectFormatter(prettyConfig); } - public String format(@Nonnull final BoltResult result) { - String infoOutput = outputFormatter.formatInfo(result.getSummary()); - String planOutput = outputFormatter.formatPlan(result.getSummary()); - String statistics = statisticsCollector.collect(result.getSummary()); - String resultOutput = outputFormatter.format(result); - String footer = outputFormatter.formatFooter(result); - return OutputFormatter.joinNonBlanks(OutputFormatter.NEWLINE, asList(infoOutput, planOutput, resultOutput, footer, statistics)); + public void format(@Nonnull final BoltResult result, LinePrinter linePrinter) { + Set capabilities = outputFormatter.capabilities(); + + if (capabilities.contains(OutputFormatter.Capablities.result)) outputFormatter.format(result, linePrinter); + + if (capabilities.contains(OutputFormatter.Capablities.info)) linePrinter.printOut(outputFormatter.formatInfo(result.getSummary())); + if (capabilities.contains(OutputFormatter.Capablities.plan)) linePrinter.printOut(outputFormatter.formatPlan(result.getSummary())); + if (capabilities.contains(OutputFormatter.Capablities.footer)) linePrinter.printOut(outputFormatter.formatFooter(result)); + if (capabilities.contains(OutputFormatter.Capablities.statistics)) linePrinter.printOut(statisticsCollector.collect(result.getSummary())); + } + + // Helper for testing + String format(@Nonnull final BoltResult result) { + StringBuilder sb = new StringBuilder(); + format(result, line -> {if (line!=null && !line.trim().isEmpty()) sb.append(line).append(OutputFormatter.NEWLINE);}); + return sb.toString(); + } + + private OutputFormatter selectFormatter(PrettyConfig prettyConfig) { + if (prettyConfig.format == Format.VERBOSE) { + return new TableOutputFormatter(prettyConfig.wrap, prettyConfig.numSampleRows); + } else { + return new SimpleOutputFormatter(); + } } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/SimpleOutputFormatter.java b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/SimpleOutputFormatter.java index 76576ee0..fd81d5f9 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/SimpleOutputFormatter.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/SimpleOutputFormatter.java @@ -5,25 +5,26 @@ import org.neo4j.driver.v1.summary.ResultSummary; import org.neo4j.shell.state.BoltResult; -import java.util.List; +import javax.annotation.Nonnull; +import java.util.EnumSet; +import java.util.Iterator; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; -import javax.annotation.Nonnull; - public class SimpleOutputFormatter implements OutputFormatter { @Override - @Nonnull - public String format(@Nonnull final BoltResult result) { - StringBuilder sb = new StringBuilder(); - List records = result.getRecords(); - if (!records.isEmpty()) { - sb.append(records.get(0).keys().stream().collect(Collectors.joining(COMMA_SEPARATOR))); - sb.append("\n"); - sb.append(records.stream().map(this::formatRecord).collect(Collectors.joining("\n"))); + public void format(@Nonnull BoltResult result, @Nonnull LinePrinter output) { + Iterator records = result.iterate(); + if (records.hasNext()) { + Record firstRow = records.next(); + output.printOut(String.join(COMMA_SEPARATOR, firstRow.keys())); + output.printOut(formatRecord(firstRow)); + while (records.hasNext()) { + output.printOut(formatRecord(records.next())); + } } - return sb.toString(); } @Nonnull @@ -41,4 +42,9 @@ public String formatInfo(@Nonnull ResultSummary summary) { return info.entrySet().stream() .map( e -> String.format("%s: %s",e.getKey(),e.getValue())).collect(Collectors.joining(NEWLINE)); } + + @Override + public Set capabilities() { + return EnumSet.of(Capablities.info, Capablities.statistics, Capablities.result); + } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/TableOutputFormatter.java b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/TableOutputFormatter.java index 4294b7f8..c5c0000e 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/TableOutputFormatter.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/prettyprint/TableOutputFormatter.java @@ -1,129 +1,175 @@ package org.neo4j.shell.prettyprint; +import org.neo4j.driver.internal.InternalRecord; +import org.neo4j.driver.v1.Record; import org.neo4j.driver.v1.Value; -import org.neo4j.driver.v1.Values; import org.neo4j.driver.v1.summary.ResultSummary; import org.neo4j.shell.state.BoltResult; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.Collections; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; +import java.util.*; +import static java.util.Arrays.asList; import static java.util.concurrent.TimeUnit.MILLISECONDS; public class TableOutputFormatter implements OutputFormatter { + private final boolean wrap; + private final int numSampleRows; + + public TableOutputFormatter(boolean wrap, int numSampleRows) { + this.wrap = wrap; + this.numSampleRows = numSampleRows; + } + @Override - @Nonnull - public String format(@Nonnull final BoltResult result) { - List data = result.getRecords().stream() - .map(r -> Values.value(r.asMap(v -> v))) - .collect(Collectors.toList()); - return formatValues(data, result.getKeys()); + public void format(@Nonnull BoltResult result, @Nonnull LinePrinter output) { + String[] columns = result.getKeys().toArray(new String[0]); + if (columns.length == 0) { + return; + } + + Iterator records = result.iterate(); + formatResult(columns, records, output); } - @Nonnull - String formatValues(@Nonnull List data, List columns) { - if (columns.isEmpty()) { - return ""; + private List take(Iterator records, int count) { + List topRecords = new ArrayList<>(count); + while (records.hasNext() && topRecords.size() < count) { + topRecords.add(records.next()); } - if (data.isEmpty()) { - return ""; + return topRecords; + } + + private void formatResult(String[] columns, + Iterator records, + LinePrinter output) { + + List topRecords = take(records, numSampleRows); + int[] columnSizes = calculateColumnSizes(columns, topRecords); + + int totalWidth = 1; + for (int columnSize : columnSizes) { + totalWidth += columnSize + 3; } - StringBuilder sb = new StringBuilder(); - Map columnSizes = calculateColumnSizes(columns, data); - String headerLine = padColumnHeading(columnSizes); - int lineWidth = headerLine.length() - 2; + StringBuilder builder = new StringBuilder(totalWidth); + String headerLine = formatRow(builder, columnSizes, columns); + int lineWidth = totalWidth - 2; String dashes = "+" + OutputFormatter.repeat('-', lineWidth) + "+"; - addHeader(sb, headerLine, dashes); - data.forEach(record -> sb.append(padColumnHeading(columnSizes, record)).append(NEWLINE)); + output.printOut(dashes); + output.printOut(headerLine); + output.printOut(dashes); - sb.append(dashes).append(NEWLINE); - return sb.toString(); + for (Record record : topRecords) { + output.printOut(formatRecord(builder, columnSizes, record)); + } + while (records.hasNext()) { + output.printOut(formatRecord(builder, columnSizes, records.next())); + } + output.printOut(dashes); } - private void addHeader(StringBuilder sb, String headerLine, String dashes) { - sb.append(dashes).append(NEWLINE); - sb.append(headerLine).append(NEWLINE); - sb.append(dashes).append(NEWLINE); + private int[] calculateColumnSizes(@Nonnull String[] columns, @Nonnull List data) { + int[] columnSizes = new int[columns.length]; + for (int i = 0; i < columns.length; i++) { + columnSizes[i] = columns[i].length(); + } + for (Record record : data) { + for (int i = 0; i < columns.length; i++) { + int len = formatValue(record.get(i)).length(); + if (columnSizes[i] < len) { + columnSizes[i] = len; + } + } + } + return columnSizes; } - @Nonnull - public String formatFooter(@Nonnull BoltResult result) { - int rows = result.getRecords().size(); - ResultSummary summary = result.getSummary(); - return String.format("%d row%s available after %d ms, " + - "consumed after another %d ms", rows, rows != 1 ? "s" : "", - summary.resultAvailableAfter(MILLISECONDS), - summary.resultConsumedAfter(MILLISECONDS)); + private String formatRecord(StringBuilder sb, int[] columnSizes, Record record) { + sb.setLength(0); + return formatRow(sb, columnSizes, formatValues(record)); } - @Nonnull - private String padColumnHeading(@Nonnull Map columnSizes, @Nonnull Value m) { - StringBuilder sb = new StringBuilder("|"); - columnSizes.entrySet().forEach(entry -> { - sb.append(" "); - String txt = formatValue(m.get(entry.getKey())); - String value = OutputFormatter.rightPad(txt, entry.getValue()); - sb.append(value); - sb.append(" |"); - }); - return sb.toString(); + private String[] formatValues(Record record) { + String[] row = new String[record.size()]; + for (int i = 0; i < row.length; i++) { + row[i] = formatValue(record.get(i)); + } + return row; } - @Nonnull - private String padColumnHeading(@Nonnull Map columnSizes) { - StringBuilder sb = new StringBuilder("|"); - for (String column : columnSizes.keySet()) { + private String formatRow(StringBuilder sb, int[] columnSizes, String[] row) { + sb.append("|"); + boolean remainder = false; + for (int i = 0; i < row.length; i++) { sb.append(" "); - sb.append(OutputFormatter.rightPad(column, columnSizes.get(column))); + int length = columnSizes[i]; + String txt = row[i]; + if (txt != null) { + if (txt.length() > length) { + if (wrap) { + sb.append(txt, 0, length); + row[i] = txt.substring(length); + remainder = true; + } else { + sb.append(txt, 0, length - 1); + sb.append("…"); + } + } else { + row[i] = null; + sb.append(OutputFormatter.rightPad(txt, length)); + } + } else { + sb.append(OutputFormatter.repeat(' ', length)); + } sb.append(" |"); } + if (wrap && remainder) { + sb.append(OutputFormatter.NEWLINE); + formatRow(sb, columnSizes, row); + } return sb.toString(); } + @Override @Nonnull - private Map calculateColumnSizes(@Nonnull List columns, @Nonnull List data) { - Map columnSizesMap = mapColumnsToLength(columns); - for (Map.Entry entry : columnSizesMap.entrySet()) { - String key = entry.getKey(); - Integer maxRecordSize = data.stream().map(record -> - formatValue(record.get(key)).length()).max(Integer::compareTo).get(); - if (entry.getValue() < maxRecordSize) { - columnSizesMap.put(key, maxRecordSize); - } - } - return columnSizesMap; - } - - private Map mapColumnsToLength(@Nonnull List columns) { - Map columnSizes = new LinkedHashMap<>(); - for (String column : columns) { - columnSizes.put(column, column.length()); - } - return columnSizes; + public String formatFooter(@Nonnull BoltResult result) { + int rows = result.getRecords().size(); + ResultSummary summary = result.getSummary(); + return String.format("%d row%s available after %d ms, " + + "consumed after another %d ms", rows, rows != 1 ? "s" : "", + summary.resultAvailableAfter(MILLISECONDS), + summary.resultConsumedAfter(MILLISECONDS)); } @Override @Nonnull public String formatInfo(@Nonnull ResultSummary summary) { Map info = OutputFormatter.info(summary); - List data = Collections.singletonList(Values.value(info)); - List columns = new ArrayList<>(info.keySet()); - return formatValues(data, columns); + if (info.isEmpty()) { + return ""; + } + String[] columns = info.keySet().toArray(new String[0]); + StringBuilder sb = new StringBuilder(); + Record record = new InternalRecord(asList(columns), info.values().toArray(new Value[0])); + formatResult(columns, Collections.singletonList(record).iterator(), sb::append); + return sb.toString(); } @Override @Nonnull public String formatPlan(@Nullable ResultSummary summary) { - if (summary == null || !summary.hasPlan()) return ""; + if (summary == null || !summary.hasPlan()) { + return ""; + } return new TablePlanFormatter().formatPlan(summary.plan()); } + + @Override + public Set capabilities() { + return EnumSet.allOf(Capablities.class); + } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/state/BoltResult.java b/cypher-shell/src/main/java/org/neo4j/shell/state/BoltResult.java index c5d1b20e..01fab213 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/state/BoltResult.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/state/BoltResult.java @@ -1,37 +1,26 @@ package org.neo4j.shell.state; import org.neo4j.driver.v1.Record; -import org.neo4j.driver.v1.StatementResult; import org.neo4j.driver.v1.summary.ResultSummary; import javax.annotation.Nonnull; +import java.util.Iterator; import java.util.List; /** - * A class holds the result from executing some Cypher. + * The result of executing some Cypher over bolt. */ -public class BoltResult { - private final List records; - private StatementResult statementResult; +public interface BoltResult { - public BoltResult(@Nonnull List records, StatementResult statementResult) { - //Not calling statementResult.list() because it executes cypher in the server - this.records = records; - this.statementResult = statementResult; - } + @Nonnull + List getKeys(); @Nonnull - public List getRecords() { - return records; - } + List getRecords(); @Nonnull - public List getKeys() { - return statementResult.keys(); - } + Iterator iterate(); @Nonnull - public ResultSummary getSummary() { - return statementResult.summary(); - } + ResultSummary getSummary(); } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java b/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java index 3048c76a..f226d880 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java @@ -26,7 +26,6 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; -import java.util.function.Function; import java.util.stream.Collectors; /** @@ -175,9 +174,7 @@ private Optional getBoltResult(@Nonnull String cypher, @Nonnull Map< return Optional.empty(); } - // calling list() is what actually executes cypher on the server - List list = statementResult.list(); - return Optional.of(new BoltResult(list, statementResult)); + return Optional.of(new StatementBoltResult(statementResult)); } /** @@ -232,8 +229,7 @@ private Optional> captureResults(@Nonnull List trans List results = executeWithRetry(transactionStatements, (statement, transaction) -> { // calling list() is what actually executes cypher on the server StatementResult sr = transaction.run(statement); - List list = sr.list(); - return new BoltResult(list, sr); + return new ListBoltResult(sr.list(), sr.consume(), sr.keys()); }); clearTransactionStatements(); diff --git a/cypher-shell/src/main/java/org/neo4j/shell/state/ListBoltResult.java b/cypher-shell/src/main/java/org/neo4j/shell/state/ListBoltResult.java new file mode 100644 index 00000000..4d0e3636 --- /dev/null +++ b/cypher-shell/src/main/java/org/neo4j/shell/state/ListBoltResult.java @@ -0,0 +1,53 @@ +package org.neo4j.shell.state; + +import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.summary.ResultSummary; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * A fully materialized Cypher result. + */ +public class ListBoltResult implements BoltResult { + + private final List keys; + private final List records; + private final ResultSummary summary; + + public ListBoltResult(@Nonnull List records, @Nonnull ResultSummary summary) { + this(records, summary, records.isEmpty() ? Collections.emptyList() : records.get(0).keys()); + } + + public ListBoltResult(@Nonnull List records, @Nonnull ResultSummary summary, @Nonnull List keys) { + this.keys = keys; + this.records = records; + this.summary = summary; + } + + @Nonnull + @Override + public List getKeys() { + return keys; + } + + @Override + @Nonnull + public List getRecords() { + return records; + } + + @Override + @Nonnull + public Iterator iterate() { + return records.iterator(); + } + + @Override + @Nonnull + public ResultSummary getSummary() { + return summary; + } +} diff --git a/cypher-shell/src/main/java/org/neo4j/shell/state/StatementBoltResult.java b/cypher-shell/src/main/java/org/neo4j/shell/state/StatementBoltResult.java new file mode 100644 index 00000000..0b1be3dd --- /dev/null +++ b/cypher-shell/src/main/java/org/neo4j/shell/state/StatementBoltResult.java @@ -0,0 +1,45 @@ +package org.neo4j.shell.state; + +import org.neo4j.driver.v1.Record; +import org.neo4j.driver.v1.StatementResult; +import org.neo4j.driver.v1.summary.ResultSummary; + +import javax.annotation.Nonnull; +import java.util.Iterator; +import java.util.List; + +/** + * Wrapper around {@link StatementResult}. Might or might not be materialized. + */ +public class StatementBoltResult implements BoltResult { + + private final StatementResult result; + + public StatementBoltResult(StatementResult result) { + this.result = result; + } + + @Nonnull + @Override + public List getKeys() { + return result.keys(); + } + + @Nonnull + @Override + public List getRecords() { + return result.list(); + } + + @Nonnull + @Override + public Iterator iterate() { + return result; + } + + @Nonnull + @Override + public ResultSummary getSummary() { + return result.summary(); + } +} diff --git a/cypher-shell/src/test/java/org/neo4j/shell/CypherShellTest.java b/cypher-shell/src/test/java/org/neo4j/shell/CypherShellTest.java index 70f77703..b41e985c 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/CypherShellTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/CypherShellTest.java @@ -8,6 +8,7 @@ import org.neo4j.driver.v1.Record; import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.Value; +import org.neo4j.driver.v1.summary.ResultSummary; import org.neo4j.shell.cli.CliArgHelper; import org.neo4j.shell.cli.CliArgs; import org.neo4j.shell.cli.StringShellRunner; @@ -15,9 +16,11 @@ import org.neo4j.shell.commands.CommandHelper; import org.neo4j.shell.exception.CommandException; import org.neo4j.shell.log.Logger; +import org.neo4j.shell.prettyprint.LinePrinter; import org.neo4j.shell.prettyprint.PrettyPrinter; import org.neo4j.shell.state.BoltResult; import org.neo4j.shell.state.BoltStateHandler; +import org.neo4j.shell.state.ListBoltResult; import org.neo4j.shell.test.OfflineTestShell; import java.io.IOException; @@ -30,12 +33,9 @@ import static org.junit.Assert.assertFalse; import static org.mockito.Matchers.anyMap; import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.contains; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; +@SuppressWarnings("OptionalGetWithoutIsPresent") public class CypherShellTest { @Rule public final ExpectedException thrown = ExpectedException.none(); @@ -135,10 +135,9 @@ public void executeOfflineThrows() throws CommandException { public void setParamShouldAddParamWithSpecialCharactersAndValue() throws CommandException { Value value = mock(Value.class); Record recordMock = mock(Record.class); - BoltResult boltResult = mock(BoltResult.class); + BoltResult boltResult = new ListBoltResult(asList(recordMock), mock(ResultSummary.class)); when(mockedBoltStateHandler.runCypher(anyString(), anyMap())).thenReturn(Optional.of(boltResult)); - when(boltResult.getRecords()).thenReturn(asList(recordMock)); when(recordMock.get("bo`b")).thenReturn(value); when(value.asObject()).thenReturn("99"); @@ -153,7 +152,7 @@ public void setParamShouldAddParamWithSpecialCharactersAndValue() throws Command public void setParamShouldAddParam() throws CommandException { Value value = mock(Value.class); Record recordMock = mock(Record.class); - BoltResult boltResult = mock(BoltResult.class); + BoltResult boltResult = mock(ListBoltResult.class); when(mockedBoltStateHandler.runCypher(anyString(), anyMap())).thenReturn(Optional.of(boltResult)); when(boltResult.getRecords()).thenReturn(asList(recordMock)); @@ -171,13 +170,13 @@ public void setParamShouldAddParam() throws CommandException { public void executeShouldPrintResult() throws CommandException { Driver mockedDriver = mock(Driver.class); Session session = mock(Session.class); - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); BoltStateHandler boltStateHandler = mock(BoltStateHandler.class); when(boltStateHandler.isConnected()).thenReturn(true); when(boltStateHandler.runCypher(anyString(), anyMap())).thenReturn(Optional.of(result)); - when(mockedPrettyPrinter.format(result)).thenReturn("999"); + doAnswer((a) -> { ((LinePrinter)a.getArguments()[1]).printOut("999"); return null;}).when(mockedPrettyPrinter).format(any(BoltResult.class), anyObject()); when(mockedDriver.session()).thenReturn(session); OfflineTestShell shell = new OfflineTestShell(logger, boltStateHandler, mockedPrettyPrinter); @@ -187,11 +186,11 @@ public void executeShouldPrintResult() throws CommandException { @Test public void commitShouldPrintResult() throws CommandException { - BoltResult result = mock(BoltResult.class); + BoltResult result = mock(ListBoltResult.class); BoltStateHandler boltStateHandler = mock(BoltStateHandler.class); - when(mockedPrettyPrinter.format(result)).thenReturn("999"); + doAnswer((a) -> { ((LinePrinter)a.getArguments()[1]).printOut("999"); return null;}).when(mockedPrettyPrinter).format(any(BoltResult.class), anyObject()); when(boltStateHandler.commitTransaction()).thenReturn(Optional.of(asList(result))); OfflineTestShell shell = new OfflineTestShell(logger, boltStateHandler, mockedPrettyPrinter); diff --git a/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgHelperTest.java b/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgHelperTest.java index 8691ab3d..25ca8502 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgHelperTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgHelperTest.java @@ -38,6 +38,22 @@ public void testForceNonInteractiveIsParsed() { CliArgHelper.parse(asArray("--non-interactive")).getNonInteractive()); } + @Test + public void testNumSampleRows() { + assertEquals("sample-rows 200", 200, CliArgHelper.parse("--sample-rows 200".split(" ")).getNumSampleRows()); + assertNull("invalid sample-rows", CliArgHelper.parse("--sample-rows 0".split(" "))); + assertNull("invalid sample-rows", CliArgHelper.parse("--sample-rows -1".split(" "))); + assertNull("invalid sample-rows", CliArgHelper.parse("--sample-rows foo".split(" "))); + } + + @Test + public void testWrap() { + assertTrue("wrap true", CliArgHelper.parse("--wrap true".split(" ")).getWrap()); + assertFalse("wrap false", CliArgHelper.parse("--wrap false".split(" ")).getWrap()); + assertTrue("default wrap", CliArgHelper.parse().getWrap()); + assertNull("invalid wrap",CliArgHelper.parse("--wrap foo".split(" "))); + } + @Test public void testDebugIsNotDefault() { assertFalse("Debug should not be the default mode", diff --git a/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgsTest.java b/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgsTest.java index acbbd314..9ee7721b 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgsTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/cli/CliArgsTest.java @@ -58,6 +58,21 @@ public void setFailBehavior() throws Exception { assertEquals(FailBehavior.FAIL_AT_END, cliArgs.getFailBehavior()); } + @Test + public void getNumSampleRows() throws Exception { + assertEquals(1000, CliArgs.DEFAULT_NUM_SAMPLE_ROWS); + assertEquals(CliArgs.DEFAULT_NUM_SAMPLE_ROWS, cliArgs.getNumSampleRows()); + + cliArgs.setNumSampleRows(null); + assertEquals(CliArgs.DEFAULT_NUM_SAMPLE_ROWS, cliArgs.getNumSampleRows()); + + cliArgs.setNumSampleRows(0); + assertEquals(CliArgs.DEFAULT_NUM_SAMPLE_ROWS, cliArgs.getNumSampleRows()); + + cliArgs.setNumSampleRows(120); + assertEquals(120, cliArgs.getNumSampleRows()); + } + @Test public void setFormat() throws Exception { // default diff --git a/cypher-shell/src/test/java/org/neo4j/shell/cli/CommandHelperTest.java b/cypher-shell/src/test/java/org/neo4j/shell/cli/CommandHelperTest.java index c2f74631..9423104d 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/cli/CommandHelperTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/cli/CommandHelperTest.java @@ -9,6 +9,7 @@ import org.neo4j.shell.commands.CommandHelper; import org.neo4j.shell.exception.CommandException; import org.neo4j.shell.log.AnsiLogger; +import org.neo4j.shell.prettyprint.PrettyConfig; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -42,7 +43,7 @@ public void shouldIgnoreCaseForCommands() { // Given AnsiLogger logger = new AnsiLogger( false ); - CommandHelper commandHelper = new CommandHelper( logger, Historian.empty, new CypherShell( logger ) ); + CommandHelper commandHelper = new CommandHelper( logger, Historian.empty, new CypherShell(logger, PrettyConfig.DEFAULT) ); // When Command begin = commandHelper.getCommand( ":BEGIN" ); diff --git a/cypher-shell/src/test/java/org/neo4j/shell/log/AnsiLoggerTest.java b/cypher-shell/src/test/java/org/neo4j/shell/log/AnsiLoggerTest.java index c3801ad9..b9a1d028 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/log/AnsiLoggerTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/log/AnsiLoggerTest.java @@ -57,7 +57,7 @@ public void printException() throws Exception { @Test public void printExceptionWithDebug() throws Exception { - logger = new AnsiLogger(true, Format.VERBOSE, out, err); + Logger logger = new AnsiLogger(true, Format.VERBOSE, out, err); logger.printError(new Throwable("bam")); verify(err).println(contains("java.lang.Throwable: bam")); verify(err).println(contains("at org.neo4j.shell.log.AnsiLoggerTest.printExceptionWithDebug")); diff --git a/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/PrettyPrinterTest.java b/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/PrettyPrinterTest.java index 3862e045..bf2188f2 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/PrettyPrinterTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/PrettyPrinterTest.java @@ -1,18 +1,21 @@ package org.neo4j.shell.prettyprint; import org.junit.Test; -import org.mockito.Matchers; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.v1.Record; import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.Values; -import org.neo4j.driver.v1.summary.*; +import org.neo4j.driver.v1.summary.ProfiledPlan; +import org.neo4j.driver.v1.summary.ResultSummary; +import org.neo4j.driver.v1.summary.StatementType; +import org.neo4j.driver.v1.summary.SummaryCounters; import org.neo4j.driver.v1.types.Node; import org.neo4j.driver.v1.types.Path; import org.neo4j.driver.v1.types.Relationship; import org.neo4j.driver.v1.util.Function; import org.neo4j.shell.cli.Format; import org.neo4j.shell.state.BoltResult; +import org.neo4j.shell.state.ListBoltResult; import java.util.Collections; import java.util.HashMap; @@ -20,6 +23,7 @@ import java.util.stream.Stream; import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; import static java.util.Collections.unmodifiableMap; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; @@ -28,21 +32,21 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.neo4j.driver.internal.util.Iterables.map; +import static org.neo4j.shell.prettyprint.OutputFormatter.NEWLINE; +@SuppressWarnings("ArraysAsListWithZeroOrOneArgument") public class PrettyPrinterTest { - private final PrettyPrinter plainPrinter = new PrettyPrinter(Format.PLAIN); - private final PrettyPrinter verbosePrinter = new PrettyPrinter(Format.VERBOSE); + private final PrettyPrinter plainPrinter = new PrettyPrinter(new PrettyConfig(Format.PLAIN, false, 100)); + private final PrettyPrinter verbosePrinter = new PrettyPrinter(new PrettyConfig(Format.VERBOSE, true, 100)); @Test - public void returnStatisticsForEmptyRecords() throws Exception { + public void returnStatisticsForEmptyRecords() { // given ResultSummary resultSummary = mock(ResultSummary.class); SummaryCounters summaryCounters = mock(SummaryCounters.class); - BoltResult result = mock(BoltResult.class); + BoltResult result = new ListBoltResult(Collections.emptyList(), resultSummary); - when(result.getRecords()).thenReturn(Collections.emptyList()); - when(result.getSummary()).thenReturn(resultSummary); when(resultSummary.counters()).thenReturn(summaryCounters); when(summaryCounters.labelsAdded()).thenReturn(1); when(summaryCounters.nodesCreated()).thenReturn(10); @@ -55,7 +59,7 @@ public void returnStatisticsForEmptyRecords() throws Exception { } @Test - public void prettyPrintProfileInformation() throws Exception { + public void prettyPrintProfileInformation() { // given ResultSummary resultSummary = mock(ResultSummary.class); ProfiledPlan plan = mock(ProfiledPlan.class); @@ -72,9 +76,7 @@ public void prettyPrintProfileInformation() throws Exception { Map argumentMap = Values.parameters("Version", "3.1", "Planner", "COST", "Runtime", "INTERPRETED").asMap(v -> v); when(plan.arguments()).thenReturn(argumentMap); - BoltResult result = mock(BoltResult.class); - when(result.getRecords()).thenReturn(Collections.emptyList()); - when(result.getSummary()).thenReturn(resultSummary); + BoltResult result = new ListBoltResult(Collections.emptyList(), resultSummary); // when String actual = plainPrinter.format(result); @@ -93,7 +95,7 @@ public void prettyPrintProfileInformation() throws Exception { } @Test - public void prettyPrintExplainInformation() throws Exception { + public void prettyPrintExplainInformation() { // given ResultSummary resultSummary = mock(ResultSummary.class); ProfiledPlan plan = mock(ProfiledPlan.class); @@ -109,9 +111,7 @@ public void prettyPrintExplainInformation() throws Exception { Map argumentMap = Values.parameters("Version", "3.1", "Planner", "COST", "Runtime", "INTERPRETED").asMap(v -> v); when(plan.arguments()).thenReturn(argumentMap); - BoltResult result = mock(BoltResult.class); - when(result.getRecords()).thenReturn(Collections.emptyList()); - when(result.getSummary()).thenReturn(resultSummary); + BoltResult result = new ListBoltResult(Collections.emptyList(), resultSummary); // when String actual = plainPrinter.format(result); @@ -128,46 +128,38 @@ public void prettyPrintExplainInformation() throws Exception { } @Test - public void prettyPrintList() throws Exception { + public void prettyPrintList() { // given - BoltResult result = mock(BoltResult.class); - Record record1 = mock(Record.class); Record record2 = mock(Record.class); - Value value1 = mock(Value.class); - Value value2 = mock(Value.class); - - - when(value1.type()).thenReturn(InternalTypeSystem.TYPE_SYSTEM.LIST()); - when(value2.type()).thenReturn(InternalTypeSystem.TYPE_SYSTEM.LIST()); - - when(value1.asList(Matchers.any(Function.class))).thenReturn(asList("val1_1", "val1_2")); - when(value2.asList(Matchers.any(Function.class))).thenReturn(asList("val2_1")); + Value value1 = Values.value("val1_1", "val1_2"); + Value value2 = Values.value(new String[]{"val2_1"}); when(record1.keys()).thenReturn(asList("col1", "col2")); when(record1.values()).thenReturn(asList(value1, value2)); when(record2.values()).thenReturn(asList(value2)); - when(result.getRecords()).thenReturn(asList(record1, record2)); - when(result.getSummary()).thenReturn(mock(ResultSummary.class)); + BoltResult result = new ListBoltResult(asList(record1, record2), mock(ResultSummary.class)); // when String actual = plainPrinter.format(result); // then - assertThat(actual, is("col1, col2\n[val1_1, val1_2], [val2_1]\n[val2_1]")); + assertThat(actual, is(String.join(NEWLINE, + "col1, col2", + "[\"val1_1\", \"val1_2\"], [\"val2_1\"]", + "[\"val2_1\"]", + ""))); } @Test - public void prettyPrintMaps() throws Exception { - checkMapForPrettyPrint(map(), "map\n{}"); - checkMapForPrettyPrint(map("abc", "def"), "map\n{abc: def}"); + public void prettyPrintMaps() { + checkMapForPrettyPrint(map(), "map"+NEWLINE+"{}"+NEWLINE); + checkMapForPrettyPrint(map("abc", "def"), "map"+NEWLINE+"{abc: def}"+NEWLINE); } private void checkMapForPrettyPrint(Map map, String expectedResult) { // given - BoltResult result = mock(BoltResult.class); - Record record = mock(Record.class); Value value = mock(Value.class); @@ -178,8 +170,7 @@ private void checkMapForPrettyPrint(Map map, String expectedResu when(record.keys()).thenReturn(asList("map")); when(record.values()).thenReturn(asList(value)); - when(result.getRecords()).thenReturn(asList(record)); - when(result.getSummary()).thenReturn(mock(ResultSummary.class)); + BoltResult result = new ListBoltResult(asList(record), mock(ResultSummary.class)); // when String actual = plainPrinter.format(result); @@ -189,10 +180,8 @@ private void checkMapForPrettyPrint(Map map, String expectedResu } @Test - public void prettyPrintNode() throws Exception { + public void prettyPrintNode() { // given - BoltResult result = mock(BoltResult.class); - Record record = mock(Record.class); Value value = mock(Value.class); @@ -210,22 +199,19 @@ public void prettyPrintNode() throws Exception { when(record.keys()).thenReturn(asList("col1", "col2")); when(record.values()).thenReturn(asList(value)); - when(result.getRecords()).thenReturn(asList(record)); - when(result.getSummary()).thenReturn(mock(ResultSummary.class)); + BoltResult result = new ListBoltResult(asList(record), mock(ResultSummary.class)); // when String actual = plainPrinter.format(result); // then - assertThat(actual, is("col1, col2\n" + - "(:label1:label2 {prop2: prop2_value, prop1: prop1_value})")); + assertThat(actual, is("col1, col2" + NEWLINE + + "(:label1:label2 {prop2: prop2_value, prop1: prop1_value})" + NEWLINE)); } @Test - public void prettyPrintRelationships() throws Exception { + public void prettyPrintRelationships() { // given - BoltResult result = mock(BoltResult.class); - Record record = mock(Record.class); Value value = mock(Value.class); @@ -243,20 +229,19 @@ public void prettyPrintRelationships() throws Exception { when(record.keys()).thenReturn(asList("rel")); when(record.values()).thenReturn(asList(value)); - when(result.getRecords()).thenReturn(asList(record)); - when(result.getSummary()).thenReturn(mock(ResultSummary.class)); + BoltResult result = new ListBoltResult(asList(record), mock(ResultSummary.class)); // when String actual = plainPrinter.format(result); // then - assertThat(actual, is("rel\n[:RELATIONSHIP_TYPE {prop2: prop2_value, prop1: prop1_value}]")); + assertThat(actual, is("rel" + NEWLINE+ + "[:RELATIONSHIP_TYPE {prop2: prop2_value, prop1: prop1_value}]" + NEWLINE)); } @Test - public void printRelationshipsAndNodesWithEscapingForSpecialCharacters() throws Exception { - BoltResult result = mock(BoltResult.class); - + public void printRelationshipsAndNodesWithEscapingForSpecialCharacters() { + // given Record record = mock(Record.class); Value relVal = mock(Value.class); Value nodeVal = mock(Value.class); @@ -272,7 +257,6 @@ public void printRelationshipsAndNodesWithEscapingForSpecialCharacters() throws nodeProp.put("1prop2", "\"\""); nodeProp.put("ä", "not-escaped"); - when(relVal.type()).thenReturn(InternalTypeSystem.TYPE_SYSTEM.RELATIONSHIP()); when(nodeVal.type()).thenReturn(InternalTypeSystem.TYPE_SYSTEM.NODE()); @@ -285,26 +269,24 @@ public void printRelationshipsAndNodesWithEscapingForSpecialCharacters() throws when(node.labels()).thenReturn(asList("label `1", "label2")); when(node.asMap(anyObject())).thenReturn(unmodifiableMap(nodeProp)); - when(record.keys()).thenReturn(asList("rel", "node")); when(record.values()).thenReturn(asList(relVal, nodeVal)); - when(result.getRecords()).thenReturn(asList(record)); - when(result.getSummary()).thenReturn(mock(ResultSummary.class)); + BoltResult result = new ListBoltResult(asList(record), mock(ResultSummary.class)); // when String actual = plainPrinter.format(result); // then - assertThat(actual, is("rel, node\n[:`RELATIONSHIP,TYPE` {prop2: prop2_value, prop1: \"prop1, value\"}], " + - "(:`label ``1`:label2 {prop1: \"prop1:value\", `1prop2`: \"\", ä: not-escaped})")); + assertThat(actual, is( + "rel, node" + NEWLINE + + "[:`RELATIONSHIP,TYPE` {prop2: prop2_value, prop1: \"prop1, value\"}], " + + "(:`label ``1`:label2 {prop1: \"prop1:value\", `1prop2`: \"\", ä: not-escaped})" + NEWLINE)); } @Test - public void prettyPrintPaths() throws Exception { + public void prettyPrintPaths() { // given - BoltResult result = mock(BoltResult.class); - Record record = mock(Record.class); Value value = mock(Value.class); @@ -312,24 +294,24 @@ public void prettyPrintPaths() throws Exception { HashMap startProperties = new HashMap<>(); startProperties.put("prop1", "prop1_value"); when(start.labels()).thenReturn(asList("start")); - when(start.id()).thenReturn(1l); + when(start.id()).thenReturn(1L); Node middle = mock(Node.class); when(middle.labels()).thenReturn(asList("middle")); - when(middle.id()).thenReturn(2l); + when(middle.id()).thenReturn(2L); Node end = mock(Node.class); HashMap endProperties = new HashMap<>(); endProperties.put("prop2", "prop2_value"); when(end.labels()).thenReturn(asList("end")); - when(end.id()).thenReturn(3l); + when(end.id()).thenReturn(3L); Path path = mock(Path.class); when(path.start()).thenReturn(start); Relationship relationship = mock(Relationship.class); when(relationship.type()).thenReturn("RELATIONSHIP_TYPE"); - when(relationship.startNodeId()).thenReturn(1l).thenReturn(3l); + when(relationship.startNodeId()).thenReturn(1L).thenReturn(3L); Path.Segment segment1 = mock(Path.Segment.class); @@ -351,41 +333,37 @@ public void prettyPrintPaths() throws Exception { when(record.keys()).thenReturn(asList("path")); when(record.values()).thenReturn(asList(value)); - when(result.getRecords()).thenReturn(asList(record)); - when(result.getSummary()).thenReturn(mock(ResultSummary.class)); + BoltResult result = new ListBoltResult(asList(record), mock(ResultSummary.class)); // when String actual = plainPrinter.format(result); // then - assertThat(actual, is("path\n" + + assertThat(actual, is("path" + NEWLINE + "(:start {prop1: prop1_value})-[:RELATIONSHIP_TYPE]->" + - "(:middle)<-[:RELATIONSHIP_TYPE]-(:end {prop2: prop2_value})")); + "(:middle)<-[:RELATIONSHIP_TYPE]-(:end {prop2: prop2_value})" + NEWLINE)); } @Test - public void prettyPrintSingleNodePath() throws Exception { + public void prettyPrintSingleNodePath() { // given - BoltResult result = mock(BoltResult.class); - Record record = mock(Record.class); Value value = mock(Value.class); Node start = mock(Node.class); when(start.labels()).thenReturn(asList("start")); - when(start.id()).thenReturn(1l); + when(start.id()).thenReturn(1L); Node end = mock(Node.class); when(end.labels()).thenReturn(asList("end")); - when(end.id()).thenReturn(2l); + when(end.id()).thenReturn(2L); Path path = mock(Path.class); when(path.start()).thenReturn(start); Relationship relationship = mock(Relationship.class); when(relationship.type()).thenReturn("RELATIONSHIP_TYPE"); - when(relationship.startNodeId()).thenReturn(1l); - + when(relationship.startNodeId()).thenReturn(1L); Path.Segment segment1 = mock(Path.Segment.class); when(segment1.start()).thenReturn(start); @@ -399,47 +377,43 @@ public void prettyPrintSingleNodePath() throws Exception { when(record.keys()).thenReturn(asList("path")); when(record.values()).thenReturn(asList(value)); - when(result.getRecords()).thenReturn(asList(record)); - when(result.getSummary()).thenReturn(mock(ResultSummary.class)); + BoltResult result = new ListBoltResult(asList(record), mock(ResultSummary.class)); // when String actual = plainPrinter.format(result); // then - assertThat(actual, is("path\n(:start)-[:RELATIONSHIP_TYPE]->(:end)")); + assertThat(actual, is("path" + NEWLINE + "(:start)-[:RELATIONSHIP_TYPE]->(:end)" + NEWLINE)); } @Test - public void prettyPrintThreeSegmentPath() throws Exception { + public void prettyPrintThreeSegmentPath() { // given - BoltResult result = mock(BoltResult.class); - Record record = mock(Record.class); Value value = mock(Value.class); Node start = mock(Node.class); when(start.labels()).thenReturn(asList("start")); - when(start.id()).thenReturn(1l); + when(start.id()).thenReturn(1L); Node second = mock(Node.class); when(second.labels()).thenReturn(asList("second")); - when(second.id()).thenReturn(2l); + when(second.id()).thenReturn(2L); Node third = mock(Node.class); when(third.labels()).thenReturn(asList("third")); - when(third.id()).thenReturn(3l); + when(third.id()).thenReturn(3L); Node end = mock(Node.class); when(end.labels()).thenReturn(asList("end")); - when(end.id()).thenReturn(4l); + when(end.id()).thenReturn(4L); Path path = mock(Path.class); when(path.start()).thenReturn(start); Relationship relationship = mock(Relationship.class); when(relationship.type()).thenReturn("RELATIONSHIP_TYPE"); - when(relationship.startNodeId()).thenReturn(1l).thenReturn(3l).thenReturn(3l); - + when(relationship.startNodeId()).thenReturn(1L).thenReturn(3L).thenReturn(3L); Path.Segment segment1 = mock(Path.Segment.class); when(segment1.start()).thenReturn(start); @@ -463,15 +437,14 @@ public void prettyPrintThreeSegmentPath() throws Exception { when(record.keys()).thenReturn(asList("path")); when(record.values()).thenReturn(asList(value)); - when(result.getRecords()).thenReturn(asList(record)); - when(result.getSummary()).thenReturn(mock(ResultSummary.class)); + BoltResult result = new ListBoltResult(singletonList(record), mock(ResultSummary.class)); // when String actual = plainPrinter.format(result); // then - assertThat(actual, is("path\n" + + assertThat(actual, is("path" + NEWLINE + "(:start)-[:RELATIONSHIP_TYPE]->" + - "(:second)<-[:RELATIONSHIP_TYPE]-(:third)-[:RELATIONSHIP_TYPE]->(:end)")); + "(:second)<-[:RELATIONSHIP_TYPE]-(:third)-[:RELATIONSHIP_TYPE]->(:end)" + NEWLINE)); } } diff --git a/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/TableOutputFormatterTest.java b/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/TableOutputFormatterTest.java index c89a882c..d26e8084 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/TableOutputFormatterTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/TableOutputFormatterTest.java @@ -27,24 +27,27 @@ import org.neo4j.driver.v1.types.Relationship; import org.neo4j.shell.cli.Format; import org.neo4j.shell.state.BoltResult; +import org.neo4j.shell.state.ListBoltResult; import java.util.*; import static java.util.Arrays.asList; import static java.util.Collections.singletonMap; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.StringContains.containsString; import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.neo4j.shell.prettyprint.OutputFormatter.NEWLINE; +@SuppressWarnings("ArraysAsListWithZeroOrOneArgument") public class TableOutputFormatterTest { - private final PrettyPrinter verbosePrinter = new PrettyPrinter(Format.VERBOSE); + private final PrettyPrinter verbosePrinter = new PrettyPrinter(new PrettyConfig(Format.VERBOSE, true, 100)); @Test - public void prettyPrintPlanInformation() throws Exception { + public void prettyPrintPlanInformation() { // given ResultSummary resultSummary = mock(ResultSummary.class); ProfiledPlan plan = mock(ProfiledPlan.class); @@ -61,9 +64,7 @@ public void prettyPrintPlanInformation() throws Exception { Map argumentMap = Values.parameters("Version", "3.1", "Planner", "COST", "Runtime", "INTERPRETED").asMap(v -> v); when(plan.arguments()).thenReturn(argumentMap); - BoltResult result = mock(BoltResult.class); - when(result.getRecords()).thenReturn(Collections.emptyList()); - when(result.getSummary()).thenReturn(resultSummary); + BoltResult result = new ListBoltResult(Collections.emptyList(), resultSummary); // when String actual = verbosePrinter.format(result); @@ -76,20 +77,16 @@ public void prettyPrintPlanInformation() throws Exception { } @Test - public void prettyPrintPoint() throws Exception { + public void prettyPrintPoint() { // given - StatementResult statementResult = mock(StatementResult.class); List keys = asList("p1", "p2"); - when(statementResult.summary()).thenReturn(mock(ResultSummary.class)); - when(statementResult.keys()).thenReturn(keys); - Value point2d = new PointValue(new InternalPoint2D(4326, 42.78, 56.7)); Value point3d = new PointValue(new InternalPoint3D(4326, 1.7, 26.79, 34.23)); Record record = new InternalRecord(keys, new Value[]{point2d, point3d}); // when - String actual = verbosePrinter.format(new BoltResult(asList(record), statementResult)); + String actual = verbosePrinter.format(new ListBoltResult(asList(record), mock(ResultSummary.class))); // then assertThat(actual, containsString("| point({srid:4326, x:42.78, y:56.7}) |")); @@ -97,71 +94,57 @@ public void prettyPrintPoint() throws Exception { } @Test - public void prettyPrintDuration() throws Exception { + public void prettyPrintDuration() { // given - StatementResult statementResult = mock(StatementResult.class); List keys = asList("d"); - when(statementResult.summary()).thenReturn(mock(ResultSummary.class)); - when(statementResult.keys()).thenReturn(keys); - Value duration = new DurationValue(new InternalIsoDuration(1, 2, 3, 4)); Record record = new InternalRecord(keys, new Value[]{duration}); // when - String actual = verbosePrinter.format(new BoltResult(asList(record), statementResult)); + String actual = verbosePrinter.format(new ListBoltResult(asList(record), mock(ResultSummary.class))); // then assertThat(actual, containsString("| P1M2DT3.000000004S |")); } @Test - public void prettyPrintDurationWithNoTrailingZeroes() throws Exception { + public void prettyPrintDurationWithNoTrailingZeroes() { // given - StatementResult statementResult = mock(StatementResult.class); List keys = asList("d"); - when(statementResult.summary()).thenReturn(mock(ResultSummary.class)); - when(statementResult.keys()).thenReturn(keys); - Value duration = new DurationValue(new InternalIsoDuration(1, 2, 3, 0)); Record record = new InternalRecord(keys, new Value[]{duration}); // when - String actual = verbosePrinter.format(new BoltResult(asList(record), statementResult)); + String actual = verbosePrinter.format(new ListBoltResult(asList(record), mock(ResultSummary.class))); // then assertThat(actual, containsString("| P1M2DT3S |")); } @Test - public void prettyPrintNode() throws Exception { + public void prettyPrintNode() { // given - StatementResult statementResult = mock(StatementResult.class); - List labels = asList("label1", "label2"); Map propertiesAsMap = new HashMap<>(); propertiesAsMap.put("prop1", Values.value("prop1_value")); propertiesAsMap.put("prop2", Values.value("prop2_value")); List keys = asList("col1", "col2"); - when(statementResult.summary()).thenReturn(mock(ResultSummary.class)); - when(statementResult.keys()).thenReturn(keys); - Value value = new NodeValue(new InternalNode(1, labels, propertiesAsMap)); Record record = new InternalRecord(keys, new Value[]{value}); // when - String actual = verbosePrinter.format(new BoltResult(asList(record), statementResult)); + String actual = verbosePrinter.format(new ListBoltResult(asList(record), mock(ResultSummary.class))); // then assertThat(actual, containsString("| (:label1:label2 {prop2: \"prop2_value\", prop1: \"prop1_value\"}) |")); } @Test - public void prettyPrintRelationships() throws Exception { + public void prettyPrintRelationships() { // given - StatementResult statementResult = mock(StatementResult.class); List keys = asList("rel"); Map propertiesAsMap = new HashMap<>(); @@ -173,20 +156,16 @@ public void prettyPrintRelationships() throws Exception { Record record = new InternalRecord(keys, new Value[]{relationship}); - when(statementResult.summary()).thenReturn(mock(ResultSummary.class)); - when(statementResult.keys()).thenReturn(keys); - // when - String actual = verbosePrinter.format(new BoltResult(asList(record), statementResult)); + String actual = verbosePrinter.format(new ListBoltResult(asList(record), mock(ResultSummary.class))); // then assertThat(actual, containsString("| [:RELATIONSHIP_TYPE {prop2: \"prop2_value\", prop1: \"prop1_value\"}] |")); } @Test - public void prettyPrintPath() throws Exception { + public void prettyPrintPath() { // given - StatementResult statementResult = mock(StatementResult.class); List keys = asList("path"); Node n1 = mock(Node.class); @@ -232,25 +211,23 @@ public void prettyPrintPath() throws Exception { Value value = new PathValue(internalPath); Record record = new InternalRecord(keys, new Value[]{value}); - when(statementResult.summary()).thenReturn(mock(ResultSummary.class)); - when(statementResult.keys()).thenReturn(keys); // when - String actual = verbosePrinter.format(new BoltResult(asList(record), statementResult)); + String actual = verbosePrinter.format(new ListBoltResult(asList(record), mock(ResultSummary.class))); // then assertThat(actual, containsString("| (:L1)<-[:R1]-(:L2)-[:R2]->(:L3) |")); } @Test - public void printRelationshipsAndNodesWithEscapingForSpecialCharacters() throws Exception { + public void printRelationshipsAndNodesWithEscapingForSpecialCharacters() { + // given Record record = mock(Record.class); Map propertiesAsMap = new HashMap<>(); propertiesAsMap.put("prop1", Values.value("prop1, value")); propertiesAsMap.put("prop2", Values.value(1)); Value relVal = new RelationshipValue(new InternalRelationship(1, 1, 2, "RELATIONSHIP,TYPE", propertiesAsMap)); - List labels = asList("label `1", "label2"); Map nodeProperties = new HashMap<>(); nodeProperties.put("prop1", Values.value("prop1:value")); @@ -265,19 +242,17 @@ public void printRelationshipsAndNodesWithEscapingForSpecialCharacters() throws recordMap.put("node", nodeVal); List keys = asList("rel", "node"); when(record.keys()).thenReturn(keys); - when(record.get(eq("rel"))).thenReturn(relVal); - when(record.get(eq("node"))).thenReturn(nodeVal); + when(record.size()).thenReturn(2); + when(record.get(0)).thenReturn(relVal); + when(record.get(1)).thenReturn(nodeVal); when(record.asMap(anyObject())).thenReturn(recordMap); when(record.values()).thenReturn(asList(relVal, nodeVal)); - StatementResult statementResult = mock(StatementResult.class); - when(statementResult.summary()).thenReturn(mock(ResultSummary.class)); - when(statementResult.keys()).thenReturn(keys); - // when - String actual = verbosePrinter.format(new BoltResult(asList(record), statementResult)); + String actual = verbosePrinter.format(new ListBoltResult(asList(record), mock(ResultSummary.class))); + // then assertThat(actual, containsString("| [:`RELATIONSHIP,TYPE` {prop2: 1, prop1: \"prop1, value\"}] |")); assertThat(actual, containsString("| (:`label ``1`:label2 {`1prop1`: \"\\\"\\\"\", " + @@ -285,7 +260,7 @@ public void printRelationshipsAndNodesWithEscapingForSpecialCharacters() throws } @Test - public void basicTable() throws Exception { + public void basicTable() { // GIVEN StatementResult result = mockResult(asList("c1", "c2"), "a", 42); // WHEN @@ -296,7 +271,7 @@ public void basicTable() throws Exception { } @Test - public void twoRows() throws Exception { + public void twoRows() { // GIVEN StatementResult result = mockResult(asList("c1", "c2"), "a", 42, "b", 43); // WHEN @@ -307,7 +282,56 @@ public void twoRows() throws Exception { } @Test - public void formatCollections() throws Exception { + public void wrapContent() + { + // GIVEN + StatementResult result = mockResult( asList( "c1"), "a", "bb","ccc","dddd","eeeee" ); + // WHEN + ToStringLinePrinter printer = new ToStringLinePrinter(); + new TableOutputFormatter(true, 2).format(new ListBoltResult(result.list(), result.summary()), printer); + String table = printer.result(); + // THEN + assertThat(table, is(String.join(NEWLINE, + "+------+", + "| c1 |", + "+------+", + "| \"a\" |", + "| \"bb\" |", + "| \"ccc |", + "| \" |", + "| \"ddd |", + "| d\" |", + "| \"eee |", + "| ee\" |", + "+------+", + ""))); + } + + @Test + public void truncateContent() + { + // GIVEN + StatementResult result = mockResult( asList( "c1"), "a", "bb","ccc","dddd","eeeee" ); + // WHEN + ToStringLinePrinter printer = new ToStringLinePrinter(); + new TableOutputFormatter(false, 2).format(new ListBoltResult(result.list(), result.summary()), printer); + String table = printer.result(); + // THEN + assertThat(table, is(String.join(NEWLINE, + "+------+", + "| c1 |", + "+------+", + "| \"a\" |", + "| \"bb\" |", + "| \"cc… |", + "| \"dd… |", + "| \"ee… |", + "+------+", + ""))); + } + + @Test + public void formatCollections() { // GIVEN StatementResult result = mockResult(asList("a", "b", "c"), singletonMap("a", 42), asList(12, 13), singletonMap("a", asList(14, 15))); @@ -318,7 +342,7 @@ public void formatCollections() throws Exception { } @Test - public void formatEntities() throws Exception { + public void formatEntities() { // GIVEN Map properties = singletonMap("name", Values.value("Mark")); Map relProperties = singletonMap("since", Values.value(2016)); @@ -335,9 +359,9 @@ public void formatEntities() throws Exception { } private String formatResult(StatementResult result) { - // calling list() is what actually executes cypher on the server - List list = result.list(); - return new TableOutputFormatter().format(new BoltResult(list, result)); + ToStringLinePrinter printer = new ToStringLinePrinter(); + new TableOutputFormatter(true, 1000).format(new ListBoltResult(result.list(), result.summary()), printer); + return printer.result(); } private StatementResult mockResult(List cols, Object... data) { diff --git a/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/ToStringLinePrinter.java b/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/ToStringLinePrinter.java new file mode 100644 index 00000000..33696938 --- /dev/null +++ b/cypher-shell/src/test/java/org/neo4j/shell/prettyprint/ToStringLinePrinter.java @@ -0,0 +1,23 @@ +package org.neo4j.shell.prettyprint; + +import javax.annotation.Nonnull; + +public class ToStringLinePrinter implements LinePrinter { + + final StringBuilder sb; + + public ToStringLinePrinter() { + this.sb = new StringBuilder(); + } + + @Override + public void printOut(String line) { + sb.append(line); + sb.append(OutputFormatter.NEWLINE); + } + + @Nonnull + public String result() { + return sb.toString(); + } +} diff --git a/cypher-shell/src/test/java/org/neo4j/shell/state/BoltStateHandlerTest.java b/cypher-shell/src/test/java/org/neo4j/shell/state/BoltStateHandlerTest.java index bffcbb21..1f4b5623 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/state/BoltStateHandlerTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/state/BoltStateHandlerTest.java @@ -179,9 +179,6 @@ public void commitPurgesTheTransactionStatementsAndCollectsResults() throws Comm Session sessionMock = mock(Session.class); Driver driverMock = stubVersionInAnOpenSession(mock(StatementResult.class), sessionMock, "neo4j-version"); - BoltResult boltResultMock1 = mock(BoltResult.class); - BoltResult boltResultMock2 = mock(BoltResult.class); - Record record1 = mock(Record.class); Record record2 = mock(Record.class); Record record3 = mock(Record.class); @@ -190,8 +187,8 @@ public void commitPurgesTheTransactionStatementsAndCollectsResults() throws Comm Value str1Val = mock(Value.class); Value str2Val = mock(Value.class); - when(boltResultMock1.getRecords()).thenReturn(asList(record1, record2)); - when(boltResultMock2.getRecords()).thenReturn(asList(record3)); + BoltResult boltResult1 = new ListBoltResult(asList(record1, record2), mock(ResultSummary.class)); + BoltResult boltResult2 = new ListBoltResult(asList(record3), mock(ResultSummary.class)); when(str1Val.toString()).thenReturn("str1"); when(str2Val.toString()).thenReturn("str2"); @@ -203,7 +200,7 @@ public void commitPurgesTheTransactionStatementsAndCollectsResults() throws Comm when(record3.get(0)).thenReturn(str1Val); when(record3.get(1)).thenReturn(str2Val); - when(sessionMock.writeTransaction(anyObject())).thenReturn(asList(boltResultMock1, boltResultMock2)); + when(sessionMock.writeTransaction(anyObject())).thenReturn(asList(boltResult1, boltResult2)); OfflineBoltStateHandler boltStateHandler = new OfflineBoltStateHandler(driverMock); boltStateHandler.connect();