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 0bfd72c4..f8010f19 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 @@ -7,17 +7,26 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; import java.io.InputStream; import java.io.PrintStream; +import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.shell.cli.CliArgs; import org.neo4j.shell.log.AnsiLogger; import org.neo4j.shell.log.Logger; +import org.neo4j.shell.prettyprint.LinePrinter; import org.neo4j.shell.prettyprint.PrettyConfig; +import org.neo4j.shell.prettyprint.ToStringLinePrinter; +import static java.lang.String.format; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; public class MainIntegrationTest { @@ -73,11 +82,10 @@ public void promptsOnWrongAuthenticationIfInteractive() throws Exception { // should be connected assertTrue(shell.isConnected()); // should have prompted and set the username and password + assertEquals( format( "username: neo4j%npassword: ***%n" ), baos.toString() ); assertEquals("neo4j", connectionConfig.username()); assertEquals("neo", connectionConfig.password()); - String out = baos.toString(); - assertEquals( String.format( "username: neo4j%npassword: ***%n" ), out ); } @Test @@ -133,9 +141,101 @@ public void wrongPortWithNeo4j() throws Exception main.connectMaybeInteractively( shell, connectionConfig, true, true ); } + @Test + public void shouldAskForCredentialsWhenConnectingWithAFile() throws Exception { + //given + + assertEquals("", connectionConfig.username()); + assertEquals("", connectionConfig.password()); + + //when + CliArgs cliArgs = new CliArgs(); + cliArgs.setInputFilename(fileFromResource("single.cypher")); + ShellAndConnection sac = getShell(cliArgs); + CypherShell shell = sac.shell; + ConnectionConfig connectionConfig = sac.connectionConfig; + main.connectMaybeInteractively( shell, connectionConfig, true, true ); + + // then we should have prompted and set the username and password + assertEquals( format( "username: neo4j%npassword: ***%n" ), baos.toString() ); + assertEquals("neo4j", connectionConfig.username()); + assertEquals("neo", connectionConfig.password()); + } + + @Test + public void shouldReadSingleCypherStatementsFromFile() throws Exception { + assertEquals(format( "result%n42%n" ), executeFileNonInteractively(fileFromResource("single.cypher"))); + } + + @Test + public void shouldReadEmptyCypherStatementsFile() throws Exception { + assertEquals("", executeFileNonInteractively(fileFromResource("empty.cypher"))); + } + + @Test + public void shouldReadMultipleCypherStatementsFromFile() throws Exception { + assertEquals(format( "result%n42%n" + + "result%n1337%n" + + "result%n\"done\"%n"), executeFileNonInteractively(fileFromResource("multiple.cypher"))); + } + + @Test + public void shouldFailIfInputFileDoesntExist() throws Exception { + // expect + exception.expect( FileNotFoundException.class); + exception.expectMessage( "what.cypher (No such file or directory)" ); + executeFileNonInteractively("what.cypher"); + } + + @Test + public void shouldHandleInvalidCypherFromFile() throws Exception { + //given + Logger logger = mock(Logger.class); + + + // when + String actual = executeFileNonInteractively( fileFromResource( "invalid.cypher" ), logger); + + //then we print the first valid row + assertEquals( format( "result%n42%n" ), actual ); + //and print errors to the error log + verify(logger).printError(any( ClientException.class )); + verifyNoMoreInteractions(logger); + } + + private String executeFileNonInteractively(String filename) throws Exception { + return executeFileNonInteractively(filename, mock(Logger.class)); + } + + private String executeFileNonInteractively(String filename, Logger logger) throws Exception + { + CliArgs cliArgs = new CliArgs(); + cliArgs.setInputFilename(filename); + + ToStringLinePrinter linePrinter = new ToStringLinePrinter(); + ShellAndConnection sac = getShell( cliArgs, linePrinter ); + CypherShell shell = sac.shell; + ConnectionConfig connectionConfig = sac.connectionConfig; + main.connectMaybeInteractively( shell, connectionConfig, true, true ); + ShellRunner shellRunner = ShellRunner.getShellRunner(cliArgs, shell, logger, connectionConfig); + shellRunner.runUntilEnd(); + + return linePrinter.result(); + } + + private String fileFromResource(String filename) + { + return getClass().getClassLoader().getResource(filename).getFile(); + } + private ShellAndConnection getShell( CliArgs cliArgs ) { Logger logger = new AnsiLogger( cliArgs.getDebugMode() ); + return getShell( cliArgs, logger ); + } + + private ShellAndConnection getShell( CliArgs cliArgs, LinePrinter linePrinter ) + { PrettyConfig prettyConfig = new PrettyConfig( cliArgs ); ConnectionConfig connectionConfig = new ConnectionConfig( cliArgs.getScheme(), @@ -146,6 +246,6 @@ private ShellAndConnection getShell( CliArgs cliArgs ) cliArgs.getEncryption(), cliArgs.getDatabase() ); - return new ShellAndConnection( new CypherShell( logger, prettyConfig, true, new ShellParameterMap() ), connectionConfig ); + return new ShellAndConnection( new CypherShell( linePrinter, prettyConfig, true, new ShellParameterMap() ), connectionConfig ); } } diff --git a/cypher-shell/src/integration-test/resources/empty.cypher b/cypher-shell/src/integration-test/resources/empty.cypher new file mode 100644 index 00000000..e69de29b diff --git a/cypher-shell/src/integration-test/resources/invalid.cypher b/cypher-shell/src/integration-test/resources/invalid.cypher new file mode 100644 index 00000000..34d53688 --- /dev/null +++ b/cypher-shell/src/integration-test/resources/invalid.cypher @@ -0,0 +1,3 @@ +RETURN 42 AS result; +THIS IS NOT CYPHER; +RETURN 1337 AS result; \ No newline at end of file diff --git a/cypher-shell/src/integration-test/resources/multiple.cypher b/cypher-shell/src/integration-test/resources/multiple.cypher new file mode 100644 index 00000000..f3919e30 --- /dev/null +++ b/cypher-shell/src/integration-test/resources/multiple.cypher @@ -0,0 +1,6 @@ +//do this first +RETURN 42 AS result; +//then we do +RETURN 1337 AS result; +//and finally we do +RETURN "done" AS result; \ No newline at end of file diff --git a/cypher-shell/src/integration-test/resources/single.cypher b/cypher-shell/src/integration-test/resources/single.cypher new file mode 100644 index 00000000..06d44f8b --- /dev/null +++ b/cypher-shell/src/integration-test/resources/single.cypher @@ -0,0 +1 @@ +RETURN 42 AS result; \ No newline at end of file diff --git a/cypher-shell/src/main/java/org/neo4j/shell/ShellRunner.java b/cypher-shell/src/main/java/org/neo4j/shell/ShellRunner.java index 769abe78..07af57cb 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/ShellRunner.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/ShellRunner.java @@ -9,7 +9,12 @@ import org.neo4j.shell.parser.ShellStatementParser; import javax.annotation.Nonnull; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import static org.fusesource.jansi.internal.CLibrary.STDIN_FILENO; import static org.fusesource.jansi.internal.CLibrary.STDOUT_FILENO; @@ -54,8 +59,9 @@ static ShellRunner getShellRunner(@Nonnull CliArgs cliArgs, return new InteractiveShellRunner(cypherShell, cypherShell, cypherShell, logger, new ShellStatementParser(), System.in, FileHistorian.getDefaultHistoryFile(), userMessagesHandler, connectionConfig); } else { + return new NonInteractiveShellRunner(cliArgs.getFailBehavior(), cypherShell, logger, - new ShellStatementParser(), System.in); + new ShellStatementParser(), getInputStream(cliArgs)); } } @@ -64,7 +70,8 @@ static ShellRunner getShellRunner(@Nonnull CliArgs cliArgs, * @return true if an interactive shellrunner should be used, false otherwise */ static boolean shouldBeInteractive(@Nonnull CliArgs cliArgs) { - if (cliArgs.getNonInteractive()) { + if ( cliArgs.getNonInteractive() || cliArgs.getInputFilename() != null ) + { return false; } @@ -114,4 +121,18 @@ static boolean isOutputInteractive() { return System.console() != null; } } + + /** + * If an input file has been defined use that, otherwise use STDIN + * @throws FileNotFoundException if the provided input file doesn't exist + */ + static InputStream getInputStream(CliArgs cliArgs) throws FileNotFoundException + { + if ( cliArgs.getInputFilename() == null) + { + return System.in; + } else { + return new BufferedInputStream( new FileInputStream( new File(cliArgs.getInputFilename()) )); + } + } } 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 119826f1..9358980e 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 @@ -98,6 +98,7 @@ private static CliArgs getCliArgs( CliArgs cliArgs, ArgumentParser parser, Names } cliArgs.setEncryption(ns.getBoolean("encryption")); cliArgs.setDatabase(ns.getString("database")); + cliArgs.setInputFilename(ns.getString( "file" ) ); //---------------- // Other arguments @@ -167,6 +168,8 @@ private static ArgumentParser setupParser(ParameterMap parameterMap) connGroup.addArgument("-d", "--database") .help("database to connect to. Can also be specified using environment variable " + ConnectionConfig.DATABASE_ENV_VAR) .setDefault(""); + connGroup.addArgument("-f", "--file") + .help("Pass a file with cypher statements to be executed. After the statements have been executed cypher-shell will be shutdown"); MutuallyExclusiveGroup failGroup = parser.addMutuallyExclusiveGroup(); failGroup.addArgument("--fail-fast") 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 08c684e6..10bba3a1 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 @@ -32,6 +32,7 @@ public class CliArgs { private boolean driverVersion = false; private int numSampleRows = DEFAULT_NUM_SAMPLE_ROWS; private boolean wrap = true; + private String inputFilename = null; private ParameterMap parameters = new ShellParameterMap(); /** @@ -111,6 +112,14 @@ public void setNonInteractive(boolean nonInteractive) { this.nonInteractive = nonInteractive; } + /** + * Sets a filename where to read Cypher statements from, much like piping statements from a file. + */ + public void setInputFilename(String inputFilename) + { + this.inputFilename = inputFilename; + } + /** * Enable/disable debug mode */ @@ -174,6 +183,11 @@ public boolean getNonInteractive() { return nonInteractive; } + public String getInputFilename() + { + return inputFilename; + } + public boolean getVersion() { return version; } diff --git a/cypher-shell/src/test/java/org/neo4j/shell/MainTest.java b/cypher-shell/src/test/java/org/neo4j/shell/MainTest.java index d9a4299b..497d421a 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/MainTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/MainTest.java @@ -199,7 +199,7 @@ public void promptsForPassIfUserExistsIfInteractive() throws Exception { } @Test - public void promptsSielntlyForPassIfUserExistsIfOutputRedirected() throws Exception { + public void promptsSilentlyForPassIfUserExistsIfOutputRedirected() throws Exception { doThrow(authException).doNothing().when(shell).connect(connectionConfig); doReturn("bob").when(connectionConfig).username(); 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 0c46cd4b..d6be162c 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 @@ -240,4 +240,17 @@ public void shouldFailForInvalidSyntaxForArg() throws Exception { CliArgHelper.parseAndThrow( "-P", "foo: => 'nanana'"); } + @Test + public void testDefaultInputFileName() { + CliArgs arguments = CliArgHelper.parse(); + assertNotNull( arguments ); + assertNull( arguments.getInputFilename() ); + } + + @Test + public void testSetInputFileName() { + CliArgs arguments = CliArgHelper.parse("--file", "foo"); + assertNotNull( arguments ); + assertEquals( "foo", arguments.getInputFilename() ); + } } 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 c3b21690..645be3f6 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 @@ -108,4 +108,11 @@ public void getParameters() // so setting them cannot be tested here. assertEquals( Collections.EMPTY_MAP, cliArgs.getParameters().allParameterValues() ); } + + @Test + public void setInputFile() + { + cliArgs.setInputFilename("foo"); + assertEquals("foo", cliArgs.getInputFilename()); + } }