ai.grakn.test.graql.shell.GraqlShellIT.java Source code

Java tutorial

Introduction

Here is the source code for ai.grakn.test.graql.shell.GraqlShellIT.java

Source

/*
 * Grakn - A Distributed Semantic Database
 * Copyright (C) 2016  Grakn Labs Limited
 *
 * Grakn is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Grakn is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Grakn. If not, see <http://www.gnu.org/licenses/gpl.txt>.
 */

package ai.grakn.test.graql.shell;

import ai.grakn.graql.GraqlShell;
import ai.grakn.test.DistributionContext;
import ai.grakn.util.GraknTestSetup;
import ai.grakn.util.Schema;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import mjson.Json;
import org.apache.commons.io.output.TeeOutputStream;
import org.hamcrest.Matcher;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.stream.Stream;

import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;

public class GraqlShellIT {

    @ClassRule
    public static final DistributionContext dist = DistributionContext.startInMemoryEngineProcess()
            .inheritIO(false);

    private static InputStream trueIn;
    private static PrintStream trueOut;
    private static PrintStream trueErr;
    private static final String expectedVersion = "graql-9.9.9";
    private static final String historyFile = System.getProperty("java.io.tmpdir") + "/graql-test-history";

    private static int keyspaceSuffix = 0;

    @BeforeClass
    public static void setUpClass() throws Exception {
        trueIn = System.in;
        trueOut = System.out;
        trueErr = System.err;

        // TODO: Get these tests working consistently on Jenkins - causes timeouts
        assumeFalse(GraknTestSetup.usingTitan());
    }

    @Before
    public void changeSuffix() {
        keyspaceSuffix += 1;
    }

    @AfterClass
    public static void resetIO() {
        System.setIn(trueIn);
        System.setOut(trueOut);
        System.setErr(trueErr);
    }

    @Test
    public void testStartAndExitShell() throws Exception {
        // Assert simply that the shell starts and terminates without errors
        assertTrue(testShell("exit\n").matches("[\\s\\S]*>>> exit(\r\n?|\n)"));
    }

    @Test
    public void testHelpOption() throws Exception {
        String result = testShell("", "--help");

        // Check for a few expected usage messages
        assertThat(result, allOf(containsString("usage"), containsString("graql.sh"), containsString("-e"),
                containsString("--execute <arg>"), containsString("query to execute")));
    }

    @Test
    public void testVersionOption() throws Exception {
        String result = testShell("", "--version");
        assertThat(result, containsString(expectedVersion));
    }

    @Test
    public void testExecuteOption() throws Exception {
        String result = testShell("", "-e", "match $x isa entity; ask;");

        // When using '-e', only results should be printed, no prompt or query
        assertThat(result,
                allOf(containsString("False"), not(containsString(">>>")), not(containsString("match"))));
    }

    @Test
    public void testDefaultKeyspace() throws Exception {
        testShell("insert im-in-the-default-keyspace sub entity;\ncommit\n");

        assertShellMatches(ImmutableList.of("-k", "grakn"), "match im-in-the-default-keyspace sub entity; ask;",
                containsString("True"));
    }

    @Test
    public void testSpecificKeyspace() throws Exception {
        testShell("insert foo-foo sub entity;\ncommit\n", "-k", "foo");
        testShell("insert bar-bar sub entity;\ncommit\n", "-k", "bar");

        String fooFooinFoo = testShell("match foo-foo sub entity; ask;\n", "-k", "foo");
        String fooFooInBar = testShell("match foo-foo sub entity; ask;\n", "-k", "bar");
        String barBarInFoo = testShell("match bar-bar sub entity; ask;\n", "-k", "foo");
        String barBarInBar = testShell("match bar-bar sub entity; ask;\n", "-k", "bar");
        assertThat(fooFooinFoo, containsString("True"));
        assertThat(fooFooInBar, containsString("False"));
        assertThat(barBarInFoo, containsString("False"));
        assertThat(barBarInBar, containsString("True"));
    }

    @Test
    public void testFileOption() throws Exception {
        ByteArrayOutputStream err = new ByteArrayOutputStream();
        testShell("", err, "-f", "src/test/graql/shell test(weird name).gql");
        assertEquals("", err.toString());
    }

    @Test
    public void testLoadCommand() throws Exception {
        assertShellMatches("load src/test/graql/shell test(weird name).gql", anything(),
                "match movie sub entity; ask;", containsString("True"));
    }

    @Test
    public void testLoadCommandWithEscapes() throws Exception {
        assertShellMatches("load src/test/graql/shell\\ test\\(weird\\ name\\).gql", anything(),
                "match movie sub entity; ask;", containsString("True"));
    }

    @Test
    public void testMatchQuery() throws Exception {
        String[] result = testShell("match $x sub concept;\nexit").split("\r\n?|\n");

        // Make sure we find a few results (don't be too fussy about the output here)
        assertEquals(">>> match $x sub concept;", result[4]);
        assertTrue(result.length > 5);
    }

    @Test
    public void testAskQuery() throws Exception {
        assertShellMatches("match $x isa relation; ask;", containsString("False"));
    }

    @Test
    public void testInsertQuery() throws Exception {
        assertShellMatches("insert entity2 sub entity;", anything(), "match $x isa entity2; ask;",
                containsString("False"), "insert $x isa entity2;", anything(), "match $x isa entity2; ask;",
                containsString("True"));
    }

    @Test
    public void testInsertOutput() throws Exception {
        assertShellMatches("insert X sub entity; $thingy isa X;",
                allOf(containsString("$thingy"), containsString("isa"), containsString("X")));
    }

    @Test
    public void testAggregateQuery() throws Exception {
        assertShellMatches("match $x sub concept; aggregate count;", is("8") // Expect to see the whole meta-ontology
        );
    }

    @Test
    public void testAutocomplete() throws Exception {
        String result = testShell("match $x isa \t");

        // Make sure all the autocompleters are working (except shell commands because we are writing a query)
        assertThat(result, allOf(containsString("concept"), containsString("match"), not(containsString("exit")),
                containsString("$x")));
    }

    @Test
    public void testAutocompleteShellCommand() throws Exception {
        String result = testShell("\t");

        // Make sure all the autocompleters are working (including shell commands because we are not writing a query)
        assertThat(result, allOf(containsString("type"), containsString("match"), containsString("exit")));
    }

    @Test
    public void testAutocompleteFill() throws Exception {
        String result = testShell("match $x sub concep\t;\n");
        assertThat(result, containsString(Schema.MetaSchema.RELATION.getLabel().getValue()));
    }

    @Test
    public void testReasonerOff() throws Exception {
        assertShellMatches("insert man sub entity has name; name sub resource datatype string;", anything(),
                "insert person sub entity;", anything(), "insert has name 'felix' isa man;", anything(),
                "insert $my-rule isa inference-rule lhs {$x isa man;} rhs {$x isa person;};", anything(), "commit",
                "match isa person, has name $x;"
        // No results
        );
    }

    @Test
    public void testReasoner() throws Exception {
        assertShellMatches(ImmutableList.of("--infer"),
                "insert man sub entity has name; name sub resource datatype string;", anything(),
                "insert person sub entity;", anything(), "insert has name 'felix' isa man;", anything(),
                "match isa person, has name $x;",
                // No results
                "insert $my-rule isa inference-rule lhs {$x isa man;} rhs {$x isa person;};", anything(), "commit",
                "match isa person, has name $x;", containsString("felix") // Results after result is added
        );
    }

    @Test
    public void testInvalidQuery() throws Exception {
        ByteArrayOutputStream err = new ByteArrayOutputStream();
        testShell("insert movie sub entity; $moon isa movie; $europa isa $moon;\n", err);

        assertThat(err.toString(), allOf(containsString("not"), containsString("type")));
    }

    @Test
    public void testComputeCount() throws Exception {
        assertShellMatches("insert X sub entity; $a isa X; $b isa X; $c isa X;", anything(), "commit",
                "compute count;", is("3"));
    }

    @Test
    public void testRollback() throws Exception {
        // Tinker graph doesn't support rollback
        assumeFalse(GraknTestSetup.usingTinker());

        String[] result = testShell("insert E sub entity;\nrollback\nmatch $x label E;\n").split("\n");

        // Make sure there are no results for match query
        assertEquals(">>> match $x label E;", result[result.length - 2]);
        assertEquals(">>> ", result[result.length - 1]);
    }

    @Test
    public void testLimit() throws Exception {
        assertShellMatches("match $x sub concept; limit 1;", anything() // Only one result
        );
    }

    @Test
    public void testGraqlOutput() throws Exception {
        String result = testShell("", "-e", "match $x sub concept;", "-o", "graql");
        assertThat(result,
                allOf(containsString("$x"), containsString(Schema.MetaSchema.ENTITY.getLabel().getValue())));
    }

    @Test
    public void testJsonOutput() throws Exception {
        String[] result = testShell("", "-e", "match $x sub concept;", "-o", "json").split("\n");
        assertTrue("expected more than 5 results: " + Arrays.toString(result), result.length > 5);
        Json json = Json.read(result[0]);
        Json x = json.at("x");
        assertTrue(x.has("id"));
        assertFalse(x.has("isa"));
    }

    @Test
    public void testHALOutput() throws Exception {
        String[] result = testShell("", "-e", "match $x sub concept;", "-o", "hal").split("\n");
        assertTrue("expected more than 5 results: " + Arrays.toString(result), result.length > 5);
        Json json = Json.read(result[0]);
        Json x = json.at("x");
        assertTrue(x.has("_id"));
        assertTrue(x.has("_baseType"));
    }

    @Test
    public void testRollbackSemicolon() throws Exception {
        // Tinker graph doesn't support rollback
        assumeFalse(GraknTestSetup.usingTinker());

        String result = testShell(
                "insert entity2 sub entity; insert $x isa entity2;\nrollback;\nmatch $x isa entity;\n");
        String[] lines = result.split("\n");

        // Make sure there are no results for match query
        assertEquals(result, ">>> match $x isa entity;", lines[lines.length - 2]);
        assertEquals(result, ">>> ", lines[lines.length - 1]);
    }

    @Test
    public void testErrorWhenEngineNotRunning() throws Exception {
        ByteArrayOutputStream err = new ByteArrayOutputStream();
        testShell("", err, "-r", "localhost:7654");

        assertFalse(err.toString().isEmpty());
    }

    @Test
    @Ignore
    /* TODO: Fix this test
     * Sometimes we see this: "Websocket closed, code: 1005, reason: null".
     * Other times, JLine crashes when receiving certain input.
     */
    public void fuzzTest() throws Exception {
        int repeats = 100;
        for (int i = 0; i < repeats; i++) {
            String input = randomString(i);
            try {
                testShellAllowErrors(input);
            } catch (Throwable e) {
                // We catch all exceptions so we can report exactly what input caused the error
                throw new RuntimeException("Error when providing the following input to shell: [" + input + "]", e);
            }
        }
    }

    @Test
    public void testLargeQuery() throws Exception {
        String value = Strings.repeat("really-", 100000) + "long-value";

        assertShellMatches("insert X sub resource datatype string; val '" + value + "' isa X;", anything(),
                "match $x isa X;", allOf(containsString("$x"), containsString(value)));
    }

    @Test
    public void whenErrorIsLarge_UserStillSeesEntireErrorMessage() throws Exception {
        String value = Strings.repeat("really-", 100000) + "long-value";

        ByteArrayOutputStream err = new ByteArrayOutputStream();

        // Query has a syntax error
        testShell("insert X sub resource datatype string; value '" + value + "' isa X;\n", err);

        assertThat(err.toString(), allOf(containsString("syntax error"), containsString(value)));
    }

    @Test
    public void testCommitError() throws Exception {
        ByteArrayOutputStream err = new ByteArrayOutputStream();
        String out = testShell("insert bob sub relation;\ncommit;\nmatch $x sub relation;\n", err);
        assertFalse(out, err.toString().isEmpty());
    }

    @Test
    public void testCommitErrorExecuteOption() throws Exception {
        ByteArrayOutputStream err = new ByteArrayOutputStream();
        String out = testShell("", err, "-e", "insert bob sub relation;");
        assertFalse(out, err.toString().isEmpty());
    }

    @Test
    public void testDefaultDontDisplayResources() throws Exception {
        assertShellMatches("insert X sub entity; R sub resource datatype string; X has R; isa X has R 'foo';",
                anything(), "match $x isa X;", allOf(containsString("id"), not(containsString("\"foo\""))));
    }

    @Test
    public void testDisplayResourcesCommand() throws Exception {
        assertShellMatches("insert X sub entity; R sub resource datatype string; X has R; isa X has R 'foo';",
                anything(), "display R;", "match $x isa X;",
                allOf(containsString("id"), containsString("\"foo\"")));
    }

    @Test
    public void whenRunningCleanCommand_TheGraphIsCleanedAndCommitted() throws Exception {
        assertShellMatches("insert my-type sub entity;", is("{}"), "commit", "match $x sub entity;",
                containsString("entity"), containsString("entity"), "clean",
                is("Are you sure? This will clean ALL data in the current keyspace and immediately commit."),
                is("Type 'confirm' to continue."), "confirm", is("Cleaning..."), "match $x sub entity;",
                containsString("entity"), "rollback", "match $x sub entity;", containsString("entity"));
    }

    @Test
    public void whenCancellingCleanCommand_TheGraphIsNotCleaned() throws Exception {
        assertShellMatches("insert my-type sub entity;", is("{}"), "match $x sub entity;", containsString("entity"),
                containsString("entity"), "clean",
                is("Are you sure? This will clean ALL data in the current keyspace and immediately commit."),
                is("Type 'confirm' to continue."), "n", is("Cancelling clean."), "match $x sub entity;",
                containsString("entity"), containsString("entity"), "clean",
                is("Are you sure? This will clean ALL data in the current keyspace and immediately commit."),
                is("Type 'confirm' to continue."), "no thanks bad idea thanks for warning me",
                is("Cancelling clean."), "match $x sub entity;", containsString("entity"),
                containsString("entity"));
    }

    @Test
    public void testExecuteMultipleQueries() throws Exception {
        assertShellMatches("insert X sub entity; $x isa X; match $y isa X; match $y isa X; aggregate count;",
                // Make sure we see results from all three queries
                containsString("$x"), containsString("$y"), is("1"));
    }

    @Test
    public void whenRunningBatchLoad_LoadCompletes() throws Exception {
        testShell("", "-k", "batch", "-f", "src/test/graql/shell test(weird name).gql");
        testShell("", "-k", "batch", "-b", "src/test/graql/batch-test.gql");

        assertShellMatches(ImmutableList.of("-k", "batch"), "match $x isa movie; ask;", containsString("True"));
    }

    @Test
    public void whenRunningBatchLoadAndAnErrorOccurs_PrintStatus() throws Exception {
        testShell("", "-k", "batch", "-f", "src/test/graql/shell test(weird name).gql");

        assertShellMatches(ImmutableList.of("-k", "batch", "-b", "src/test/graql/batch-test-bad.gql"),
                is("Status of batch: FAILED"), is("Number batches completed: 1"),
                containsString("Approximate queries executed:"), containsString("All tasks completed"));
    }

    @Test
    public void whenUserMakesAMistake_SubsequentQueriesStillWork() throws Exception {
        ByteArrayOutputStream err = new ByteArrayOutputStream();
        String out = testShell("match $x sub concet; aggregate count;\n" + "match $x sub concept; ask;\n", err);

        assertThat(err.toString(), not(containsString("error")));
        assertThat(out, containsString("True"));
    }

    @Test
    public void testDuplicateRelation() throws Exception {
        ByteArrayOutputStream err = new ByteArrayOutputStream();
        testShell("insert R sub relation, relates R1, relates R2; R1 sub role; R2 sub role;\n"
                + "insert X sub entity, plays R1, plays R2;\n" + "insert $x isa X; (R1: $x, R2: $x) isa R;\n"
                + "match $x isa X; insert (R1: $x, R2: $x) isa R;\n" + "commit\n", err);

        assertThat(err.toString().toLowerCase(),
                allOf(anyOf(containsString("exists"), containsString("one or more")), containsString("relation")));
    }

    private static String randomString(int length) {
        Random random = new Random();
        StringBuilder sb = new StringBuilder();

        random.ints().limit(length).forEach(i -> sb.append((char) i));

        return sb.toString();
    }

    private void assertShellMatches(Object... matchers) throws Exception {
        assertShellMatches(ImmutableList.of(), matchers);
    }

    // Arguments should be strings or matchers. Strings are interpreted as input, matchers as expected output
    private void assertShellMatches(List<String> arguments, Object... matchers) throws Exception {
        String input = Stream.of(matchers).filter(String.class::isInstance).map(String.class::cast)
                .collect(joining("\n", "", "\n"));

        List<Matcher<? super String>> matcherList = Stream.of(matchers)
                .map(obj -> (obj instanceof Matcher) ? (Matcher<String>) obj : is(">>> " + obj)).collect(toList());

        String output = testShell(input, arguments.toArray(new String[arguments.size()]));

        List<String> outputLines = Lists.newArrayList(output.replace(" \r", "").split("\n"));

        ImmutableSet<String> noPromptArgs = ImmutableSet.of("-e", "-f", "-b", "-v", "-h");
        if (Sets.intersection(Sets.newHashSet(arguments), noPromptArgs).isEmpty()) {
            // Remove first four lines containing license and last line containing prompt
            outputLines = outputLines.subList(4, outputLines.size() - 1);
        }

        assertThat(outputLines, contains(matcherList));
    }

    private String testShell(String input, String... args) throws Exception {
        ByteArrayOutputStream err = new ByteArrayOutputStream();
        String result = testShell(input, err, args);
        String errMessage = err.toString();
        assertTrue("Error: \"" + errMessage + "\"", errMessage.isEmpty());
        return result;
    }

    private String testShellAllowErrors(String input, String... args) throws Exception {
        ByteArrayOutputStream err = new ByteArrayOutputStream();
        return testShell(input, err, args);
    }

    private String testShell(String input, ByteArrayOutputStream berr, String... args) throws Exception {
        args = specifyUniqueKeyspace(args);

        InputStream in = new ByteArrayInputStream(input.getBytes());

        ByteArrayOutputStream bout = new ByteArrayOutputStream();

        PrintStream out = new PrintStream(new TeeOutputStream(bout, trueOut));

        // Intercept stderr, but make sure it is still printed using the TeeOutputStream
        PrintStream err = new PrintStream(new TeeOutputStream(berr, trueErr));

        try {
            System.out.flush();
            System.err.flush();
            System.setIn(in);
            System.setOut(out);
            System.setErr(err);

            GraqlShell.runShell(args, expectedVersion, historyFile);
        } catch (Exception e) {
            System.setErr(trueErr);
            e.printStackTrace();
            err.flush();
            fail(berr.toString());
        } finally {
            resetIO();
        }

        out.flush();
        err.flush();

        return bout.toString();
    }

    // TODO: Remove this when we can clear graphs properly (TP #13745)
    private String[] specifyUniqueKeyspace(String[] args) {
        List<String> argList = Lists.newArrayList(args);

        int keyspaceIndex = argList.indexOf("-k") + 1;
        if (keyspaceIndex == 0) {
            argList.add("-k");
            argList.add(GraqlShell.DEFAULT_KEYSPACE);
            keyspaceIndex = argList.size() - 1;
        }

        argList.set(keyspaceIndex, argList.get(keyspaceIndex) + keyspaceSuffix);

        return argList.toArray(new String[argList.size()]);
    }
}