From 135ff87862ff49c04873ba143ee8b3cb48a3e9c4 Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Thu, 12 Sep 2019 10:18:26 +0200 Subject: [PATCH 1/2] Support for :source [file] in cypher-shell Adds support for the command `:source` where you can in interactive mode run statements from an external file. --- .../org/neo4j/shell/MainIntegrationTest.java | 97 ++++++++++++++++++- .../neo4j/shell/commands/CommandHelper.java | 23 ++--- .../java/org/neo4j/shell/commands/Source.java | 81 ++++++++++++++++ .../org/neo4j/shell/commands/SourceTest.java | 91 +++++++++++++++++ .../org/neo4j/shell/commands/test.cypher | 1 + 5 files changed, 279 insertions(+), 14 deletions(-) create mode 100644 cypher-shell/src/main/java/org/neo4j/shell/commands/Source.java create mode 100644 cypher-shell/src/test/java/org/neo4j/shell/commands/SourceTest.java create mode 100644 cypher-shell/src/test/resources/org/neo4j/shell/commands/test.cypher 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 f8010f19..c16ff0ff 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 @@ -14,6 +14,9 @@ import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.shell.cli.CliArgs; +import org.neo4j.shell.commands.CommandHelper; +import org.neo4j.shell.exception.CommandException; +import org.neo4j.shell.exception.ExitException; import org.neo4j.shell.log.AnsiLogger; import org.neo4j.shell.log.Logger; import org.neo4j.shell.prettyprint.LinePrinter; @@ -21,8 +24,10 @@ import org.neo4j.shell.prettyprint.ToStringLinePrinter; import static java.lang.String.format; +import static org.hamcrest.CoreMatchers.isA; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -85,7 +90,6 @@ public void promptsOnWrongAuthenticationIfInteractive() throws Exception { assertEquals( format( "username: neo4j%npassword: ***%n" ), baos.toString() ); assertEquals("neo4j", connectionConfig.username()); assertEquals("neo", connectionConfig.password()); - } @Test @@ -203,6 +207,75 @@ public void shouldHandleInvalidCypherFromFile() throws Exception { verifyNoMoreInteractions(logger); } + @Test + public void shouldReadSingleCypherStatementsFromFileInteractively() throws Exception { + // given + ToStringLinePrinter linePrinter = new ToStringLinePrinter(); + CypherShell shell = interactiveShell( linePrinter ); + + // when + shell.execute( ":source " + fileFromResource( "single.cypher" )); + exit( shell ); + + // then + assertEquals( format("result%n42%n"), linePrinter.result() ); + } + + @Test + public void shouldReadMultipleCypherStatementsFromFileInteractively() throws Exception { + // given + ToStringLinePrinter linePrinter = new ToStringLinePrinter(); + CypherShell shell = interactiveShell( linePrinter ); + + // when + shell.execute( ":source " + fileFromResource( "multiple.cypher" )); + exit( shell ); + + // then + assertEquals(format( "result%n42%n" + + "result%n1337%n" + + "result%n\"done\"%n"), linePrinter.result() ); + } + + @Test + public void shouldReadEmptyCypherStatementsFromFileInteractively() throws Exception { + // given + ToStringLinePrinter linePrinter = new ToStringLinePrinter(); + CypherShell shell = interactiveShell( linePrinter ); + + // when + shell.execute( ":source " + fileFromResource( "empty.cypher" )); + exit( shell ); + + // then + assertEquals("", linePrinter.result() ); + } + + @Test + public void shouldReadHandleInvalidCypherStatementsFromFileInteractively() throws Exception { + // given + ToStringLinePrinter linePrinter = new ToStringLinePrinter(); + CypherShell shell = interactiveShell( linePrinter ); + + // then + exception.expect( ClientException.class ); + exception.expectMessage( "Invalid input 'T':" ); + shell.execute( ":source " + fileFromResource( "invalid.cypher" )); + } + + @Test + public void shouldFailIfInputFileDoesntExistInteractively() throws Exception { + // given + ToStringLinePrinter linePrinter = new ToStringLinePrinter(); + CypherShell shell = interactiveShell( linePrinter ); + + // expect + exception.expect( CommandException.class); + exception.expectMessage( "Cannot find file: 'what.cypher'" ); + exception.expectCause( isA( FileNotFoundException.class ) ); + shell.execute( ":source what.cypher" ); + } + private String executeFileNonInteractively(String filename) throws Exception { return executeFileNonInteractively(filename, mock(Logger.class)); } @@ -228,6 +301,15 @@ private String fileFromResource(String filename) return getClass().getClassLoader().getResource(filename).getFile(); } + private CypherShell interactiveShell( LinePrinter linePrinter ) throws Exception + { + PrettyConfig prettyConfig = new PrettyConfig( new CliArgs() ); + CypherShell shell = new CypherShell( linePrinter, prettyConfig, true, new ShellParameterMap() ); + main.connectMaybeInteractively( shell, connectionConfig, true, true ); + shell.setCommandHelper( new CommandHelper( mock( Logger.class ), Historian.empty, shell) ); + return shell; + } + private ShellAndConnection getShell( CliArgs cliArgs ) { Logger logger = new AnsiLogger( cliArgs.getDebugMode() ); @@ -248,4 +330,17 @@ private ShellAndConnection getShell( CliArgs cliArgs, LinePrinter linePrinter ) return new ShellAndConnection( new CypherShell( linePrinter, prettyConfig, true, new ShellParameterMap() ), connectionConfig ); } + + private void exit( CypherShell shell ) throws CommandException + { + try + { + shell.execute( ":exit" ); + fail("Should have exited"); + } + catch ( ExitException e ) + { + //do nothing + } + } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/commands/CommandHelper.java b/cypher-shell/src/main/java/org/neo4j/shell/commands/CommandHelper.java index 42e52f5f..b2972eb9 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/commands/CommandHelper.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/commands/CommandHelper.java @@ -7,14 +7,12 @@ import javax.annotation.Nullable; import org.neo4j.shell.CypherShell; -import org.neo4j.shell.DatabaseManager; import org.neo4j.shell.Historian; -import org.neo4j.shell.TransactionHandler; -import org.neo4j.shell.ParameterMap; import org.neo4j.shell.exception.CommandException; import org.neo4j.shell.exception.DuplicateCommandException; import org.neo4j.shell.log.AnsiFormattedText; import org.neo4j.shell.log.Logger; +import org.neo4j.shell.parser.ShellStatementParser; /** * Utility methods for dealing with commands @@ -23,23 +21,22 @@ public class CommandHelper { private final TreeMap commands = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); public CommandHelper(Logger logger, Historian historian, CypherShell cypherShell) { - registerAllCommands(logger, historian, cypherShell, cypherShell, cypherShell.getParameterMap()); + registerAllCommands(logger, historian, cypherShell); } private void registerAllCommands(Logger logger, Historian historian, - DatabaseManager databaseManager, - TransactionHandler transactionHandler, - ParameterMap parameterMap) { + CypherShell cypherShell) { registerCommand(new Exit(logger)); registerCommand(new Help(logger, this)); registerCommand(new History(logger, historian)); - registerCommand(new Use(databaseManager)); - registerCommand(new Begin(transactionHandler)); - registerCommand(new Commit(transactionHandler)); - registerCommand(new Rollback(transactionHandler)); - registerCommand(new Param(parameterMap)); - registerCommand(new Params(logger, parameterMap)); + registerCommand(new Use(cypherShell)); + registerCommand(new Begin(cypherShell)); + registerCommand(new Commit(cypherShell)); + registerCommand(new Rollback(cypherShell)); + registerCommand(new Param(cypherShell.getParameterMap())); + registerCommand(new Params(logger, cypherShell.getParameterMap())); + registerCommand(new Source(cypherShell, new ShellStatementParser() )); } private void registerCommand(@Nonnull final Command command) throws DuplicateCommandException { diff --git a/cypher-shell/src/main/java/org/neo4j/shell/commands/Source.java b/cypher-shell/src/main/java/org/neo4j/shell/commands/Source.java new file mode 100644 index 00000000..76153a44 --- /dev/null +++ b/cypher-shell/src/main/java/org/neo4j/shell/commands/Source.java @@ -0,0 +1,81 @@ +package org.neo4j.shell.commands; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nonnull; + +import org.neo4j.shell.CypherShell; +import org.neo4j.shell.exception.CommandException; +import org.neo4j.shell.exception.ExitException; +import org.neo4j.shell.parser.StatementParser; + +import static java.lang.String.format; +import static org.neo4j.shell.commands.CommandHelper.simpleArgParse; + +/** + * This command reads a cypher file frome the filesystem and executes the statements therein. + */ +public class Source implements Command { + private static final String COMMAND_NAME = ":source"; + private final CypherShell cypherShell; + private final StatementParser statementParser; + + public Source( CypherShell cypherShell, StatementParser statementParser ) { + this.cypherShell = cypherShell; + this.statementParser = statementParser; + } + + @Nonnull + @Override + public String getName() { + return COMMAND_NAME; + } + + @Nonnull + @Override + public String getDescription() { + return "Interactively executes cypher statements from a file"; + } + + @Nonnull + @Override + public String getUsage() { + return "[filename]"; + } + + @Nonnull + @Override + public String getHelp() { + return "Executes Cypher statements from a file"; + } + + @Nonnull + @Override + public List getAliases() { + return Collections.emptyList(); + } + + @Override + public void execute(@Nonnull final String argString) throws ExitException, CommandException { + String filename = simpleArgParse(argString, 1, 1, COMMAND_NAME, getUsage())[0]; + + try ( BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream( new File(filename) )))) { + bufferedReader.lines() + .forEach(line -> statementParser.parseMoreText(line + "\n")); + List statements = statementParser.consumeStatements(); + for ( String statement : statements ) + { + cypherShell.execute( statement ); + } + } + catch ( IOException e ) + { + throw new CommandException( format("Cannot find file: '%s'", filename), e); + } + } +} diff --git a/cypher-shell/src/test/java/org/neo4j/shell/commands/SourceTest.java b/cypher-shell/src/test/java/org/neo4j/shell/commands/SourceTest.java new file mode 100644 index 00000000..b4baf1d2 --- /dev/null +++ b/cypher-shell/src/test/java/org/neo4j/shell/commands/SourceTest.java @@ -0,0 +1,91 @@ +package org.neo4j.shell.commands; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.io.FileNotFoundException; + +import org.neo4j.shell.CypherShell; +import org.neo4j.shell.exception.CommandException; +import org.neo4j.shell.log.Logger; +import org.neo4j.shell.parser.ShellStatementParser; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.isA; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class SourceTest +{ + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private Logger logger; + private Source cmd; + private CypherShell shell; + + @Before + public void setup() + { + logger = mock(Logger.class); + shell = mock(CypherShell.class); + cmd = new Source(shell, new ShellStatementParser() ); + } + + @Test + public void descriptionNotNull() { + assertNotNull(cmd.getDescription()); + } + + @Test + public void usageNotNull() { + assertNotNull(cmd.getUsage()); + } + + @Test + public void helpNotNull() { + assertNotNull(cmd.getHelp()); + } + + @Test + public void runCommand() throws CommandException { + // given + cmd.execute( fileFromResource( "test.cypher" ) ); + verify(shell).execute( "RETURN 42;" ); + verifyNoMoreInteractions( shell ); + } + + @Test + public void shouldFailIfFileNotThere() throws CommandException + { + thrown.expect( CommandException.class ); + thrown.expectMessage(containsString("Cannot find file: 'not.there'")); + thrown.expectCause(isA(FileNotFoundException.class)); + cmd.execute( "not.there" ); + } + + @Test + public void shouldNotAcceptMoreThanOneArgs() throws CommandException { + thrown.expect(CommandException.class); + thrown.expectMessage(containsString("Incorrect number of arguments")); + + cmd.execute("bob sob"); + } + + @Test + public void shouldNotAcceptZeroArgs() throws CommandException { + thrown.expect(CommandException.class); + thrown.expectMessage(containsString("Incorrect number of arguments")); + + cmd.execute(""); + } + + private String fileFromResource(String filename) + { + return getClass().getResource(filename).getFile(); + } +} diff --git a/cypher-shell/src/test/resources/org/neo4j/shell/commands/test.cypher b/cypher-shell/src/test/resources/org/neo4j/shell/commands/test.cypher new file mode 100644 index 00000000..2938e6ee --- /dev/null +++ b/cypher-shell/src/test/resources/org/neo4j/shell/commands/test.cypher @@ -0,0 +1 @@ +RETURN 42; \ No newline at end of file From 57439958622416b52170a0c3f906d11d856e83f6 Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Thu, 12 Sep 2019 10:44:30 +0200 Subject: [PATCH 2/2] Code Review: typo --- .../java/org/neo4j/shell/MainIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c16ff0ff..a5506571 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 @@ -252,7 +252,7 @@ public void shouldReadEmptyCypherStatementsFromFileInteractively() throws Except } @Test - public void shouldReadHandleInvalidCypherStatementsFromFileInteractively() throws Exception { + public void shouldHandleInvalidCypherStatementsFromFileInteractively() throws Exception { // given ToStringLinePrinter linePrinter = new ToStringLinePrinter(); CypherShell shell = interactiveShell( linePrinter );