/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.tinkerpop.gremlin.process.computer;

import org.apache.commons.configuration2.BaseConfiguration;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.ConfigurationUtils;
import org.apache.tinkerpop.gremlin.ExceptionCoverage;
import org.apache.tinkerpop.gremlin.LoadGraphWith;
import org.apache.tinkerpop.gremlin.process.AbstractGremlinProcessTest;
import org.apache.tinkerpop.gremlin.process.computer.clustering.peerpressure.PeerPressureVertexProgram;
import org.apache.tinkerpop.gremlin.process.computer.ranking.pagerank.PageRankVertexProgram;
import org.apache.tinkerpop.gremlin.process.computer.traversal.TraversalVertexProgram;
import org.apache.tinkerpop.gremlin.process.computer.util.AbstractVertexProgramBuilder;
import org.apache.tinkerpop.gremlin.process.computer.util.StaticMapReduce;
import org.apache.tinkerpop.gremlin.process.computer.util.StaticVertexProgram;
import org.apache.tinkerpop.gremlin.process.traversal.Operator;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.EmptyPath;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.VerificationException;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.util.TraverserSet;
import org.apache.tinkerpop.gremlin.structure.Direction;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
import org.javatuples.Pair;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import static org.apache.tinkerpop.gremlin.LoadGraphWith.GraphData.GRATEFUL;
import static org.apache.tinkerpop.gremlin.LoadGraphWith.GraphData.MODERN;
import static org.apache.tinkerpop.gremlin.LoadGraphWith.GraphData.SINK;
import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.outE;
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.assumeNoException;

/**
 * @author Marko A. Rodriguez (http://markorodriguez.com)
 */
@ExceptionCoverage(exceptionClass = GraphComputer.Exceptions.class, methods = {
        "providedKeyIsNotAMemoryComputeKey",
        "computerHasNoVertexProgramNorMapReducers",
        "computerHasAlreadyBeenSubmittedAVertexProgram",
        "providedKeyIsNotAnElementComputeKey",
        "incidentAndAdjacentElementsCanNotBeAccessedInMapReduce",
        "adjacentVertexLabelsCanNotBeRead",
        "adjacentVertexPropertiesCanNotBeReadOrUpdated",
        "adjacentVertexEdgesAndVerticesCanNotBeReadOrUpdated",
        "resultGraphPersistCombinationNotSupported",
        "vertexPropertiesCanNotBeUpdatedInMapReduce",
        "computerRequiresMoreWorkersThanSupported",
        "vertexFilterAccessesIncidentEdges",
        "edgeFilterAccessesAdjacentVertices",
        "graphFilterNotSupported"
})
@ExceptionCoverage(exceptionClass = Memory.Exceptions.class, methods = {
        "memoryKeyCanNotBeEmpty",
        "memoryKeyCanNotBeNull",
        "memoryValueCanNotBeNull",
        "memoryIsCurrentlyImmutable",
        "memoryDoesNotExist",
        "memorySetOnlyDuringVertexProgramSetUpAndTerminate",
        "memoryAddOnlyDuringVertexProgramExecute",
        "adjacentVertexEdgesAndVerticesCanNotBeReadOrUpdated"
})
@ExceptionCoverage(exceptionClass = Graph.Exceptions.class, methods = {
        "graphDoesNotSupportProvidedGraphComputer"
})
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public class GraphComputerTest extends AbstractGremlinProcessTest {

    @Test
    @LoadGraphWith(MODERN)
    public void shouldHaveStandardStringRepresentation() {
        final GraphComputer computer = graphProvider.getGraphComputer(graph);
        assertEquals(StringFactory.graphComputerString(computer), computer.toString());
    }

    @Test
    @LoadGraphWith(MODERN)
    public void shouldNotAllowWithNoVertexProgramNorMapReducers() throws Exception {
        try {
            graphProvider.getGraphComputer(graph).submit().get();
            fail("Should throw an IllegalStateException when there is no vertex program nor map reducers");
        } catch (Exception ex) {
            validateException(GraphComputer.Exceptions.computerHasNoVertexProgramNorMapReducers(), ex);
        }
    }


    /////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldNotAllowBadGraphComputers() {
        try {
            graph.compute(BadGraphComputer.class);
            fail("Providing a bad graph computer class should fail");
        } catch (Exception ex) {
            validateException(Graph.Exceptions.graphDoesNotSupportProvidedGraphComputer(BadGraphComputer.class), ex);
        }
        if (!new BadGraphComputer().features().supportsGraphFilter()) {
            try {
                new BadGraphComputer().vertices(__.hasLabel("software"));
                fail("Should throw an unsupported operation exception");
            } catch (final UnsupportedOperationException e) {
                assertEquals(GraphComputer.Exceptions.graphFilterNotSupported().getMessage(), e.getMessage());
            }
            try {
                new BadGraphComputer().edges(__.bothE());
                fail("Should throw an unsupported operation exception");
            } catch (final UnsupportedOperationException e) {
                assertEquals(GraphComputer.Exceptions.graphFilterNotSupported().getMessage(), e.getMessage());
            }
        } else {
            fail("Should not support graph filter: " + BadGraphComputer.class);
        }
    }

    public static class BadGraphComputer implements GraphComputer {

        @Override
        public GraphComputer result(final ResultGraph resultGraph) {
            return null;
        }

        @Override
        public GraphComputer persist(final Persist persist) {
            return null;
        }

        @Override
        public GraphComputer program(final VertexProgram vertexProgram) {
            return null;
        }

        @Override
        public GraphComputer mapReduce(final MapReduce mapReduce) {
            return null;
        }

        @Override
        public GraphComputer workers(final int workers) {
            return null;
        }

        @Override
        public GraphComputer vertices(final Traversal<Vertex, Vertex> vertexFilter) {
            throw GraphComputer.Exceptions.graphFilterNotSupported();
        }

        @Override
        public GraphComputer edges(final Traversal<Vertex, Edge> edgeFilter) {
            throw GraphComputer.Exceptions.graphFilterNotSupported();
        }

        @Override
        public GraphComputer configure(final String key, final Object value) {
            return null;
        }

        @Override
        public Future<ComputerResult> submit() {
            return null;
        }

        public GraphComputer.Features features() {
            return new Features() {
                @Override
                public boolean supportsGraphFilter() {
                    return false;
                }
            };
        }
    }
    /////////////////////////////////////////////

    /////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldHaveImmutableComputeResultMemory() throws Exception {
        final ComputerResult results = graphProvider.getGraphComputer(graph).program(new VertexProgramB()).submit().get();

        try {
            results.memory().set("set", "test");
        } catch (Exception ex) {
            validateException(Memory.Exceptions.memoryIsCurrentlyImmutable(), ex);
        }

        try {
            results.memory().add("incr", 1);
        } catch (Exception ex) {
            validateException(Memory.Exceptions.memoryIsCurrentlyImmutable(), ex);
        }

        try {
            results.memory().add("and", true);
        } catch (Exception ex) {
            validateException(Memory.Exceptions.memoryIsCurrentlyImmutable(), ex);
        }

        try {
            results.memory().add("or", false);
        } catch (Exception ex) {
            validateException(Memory.Exceptions.memoryIsCurrentlyImmutable(), ex);
        }
    }

    public static class VertexProgramB extends StaticVertexProgram {
        @Override
        public void setup(final Memory memory) {
            assertEquals(0, memory.getIteration());
            assertTrue(memory.isInitialIteration());
        }

        @Override
        public void execute(final Vertex vertex, final Messenger messenger, final Memory memory) {
            assertEquals(0, memory.getIteration());
            assertTrue(memory.isInitialIteration());
        }

        @Override
        public boolean terminate(final Memory memory) {
            assertEquals(0, memory.getIteration());
            assertTrue(memory.isInitialIteration());
            return true;
        }

        @Override
        public Set<MemoryComputeKey> getMemoryComputeKeys() {
            return new HashSet<>(Arrays.asList(
                    MemoryComputeKey.of("set", Operator.assign, true, false),
                    MemoryComputeKey.of("incr", Operator.sum, true, false),
                    MemoryComputeKey.of("and", Operator.and, true, false),
                    MemoryComputeKey.of("or", Operator.or, true, false)));
        }

        @Override
        public Set<MessageScope> getMessageScopes(final Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.ORIGINAL;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.NOTHING;
        }
    }
    /////////////////////////////////////////////

    @Test
    @LoadGraphWith(MODERN)
    public void shouldNotAllowNullMemoryKeys() throws Exception {
        try {
            graphProvider.getGraphComputer(graph).program(new VertexProgramC()).submit().get();
            fail("Providing null memory key should fail");
        } catch (Exception ex) {
            // validateException(Memory.Exceptions.memoryKeyCanNotBeNull(), ex);
        }
    }

    public static class VertexProgramC extends StaticVertexProgram {
        @Override
        public void setup(final Memory memory) {

        }

        @Override
        public void execute(final Vertex vertex, final Messenger messenger, final Memory memory) {

        }

        @Override
        public boolean terminate(final Memory memory) {
            return true;
        }

        @Override
        public Set<MemoryComputeKey> getMemoryComputeKeys() {
            return Collections.singleton(MemoryComputeKey.of(null, Operator.or, true, false));
        }

        @Override
        public Set<MessageScope> getMessageScopes(final Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.ORIGINAL;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.NOTHING;
        }
    }
    /////////////////////////////////////////////

    /////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldNotAllowEmptyMemoryKeys() throws Exception {
        try {
            graphProvider.getGraphComputer(graph).program(new VertexProgramD()).submit().get();
            fail("Providing empty memory key should fail");
        } catch (Exception ex) {
            validateException(Memory.Exceptions.memoryKeyCanNotBeEmpty(), ex);
        }
    }

    public static class VertexProgramD extends StaticVertexProgram {
        @Override
        public void setup(final Memory memory) {

        }

        @Override
        public void execute(final Vertex vertex, final Messenger messenger, final Memory memory) {

        }

        @Override
        public boolean terminate(final Memory memory) {
            return true;
        }

        @Override
        public Set<MemoryComputeKey> getMemoryComputeKeys() {
            return Collections.singleton(MemoryComputeKey.of("", Operator.or, true, false));
        }

        @Override
        public Set<MessageScope> getMessageScopes(final Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.ORIGINAL;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.NOTHING;
        }
    }
    ////////////////////////////////////////////

    ////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldHandleUndeclaredMemoryKeysCorrectly() throws Exception {
        graphProvider.getGraphComputer(graph).program(new VertexProgramE()).submit().get();
    }

    public static class VertexProgramE extends StaticVertexProgram {
        @Override
        public void setup(final Memory memory) {
            try {
                memory.get("a");
                fail("The memory key does not exist and should fail");
            } catch (Exception e) {
                validateException(Memory.Exceptions.memoryDoesNotExist("a"), e);
            }
            try {
                memory.set("a", true);
                fail("Setting a memory key that wasn't declared should fail");
            } catch (Exception e) {
                validateException(GraphComputer.Exceptions.providedKeyIsNotAMemoryComputeKey("a"), e);
            }
        }

        @Override
        public void execute(final Vertex vertex, final Messenger messenger, final Memory memory) {
            try {
                memory.get("a");
                fail("The memory key does not exist and should fail");
            } catch (Exception e) {
                validateException(Memory.Exceptions.memoryDoesNotExist("a"), e);
            }
            try {
                memory.add("a", true);
                fail("Setting a memory key that wasn't declared should fail");
            } catch (Exception e) {
                validateException(GraphComputer.Exceptions.providedKeyIsNotAMemoryComputeKey("a"), e);
            }
        }

        @Override
        public boolean terminate(final Memory memory) {
            try {
                memory.get("a");
                fail("The memory key does not exist and should fail");
            } catch (Exception e) {
                validateException(Memory.Exceptions.memoryDoesNotExist("a"), e);
            }
            try {
                memory.set("a", true);
                // fail("Setting a memory key that wasn't declared should fail");
            } catch (Exception e) {
                validateException(GraphComputer.Exceptions.providedKeyIsNotAMemoryComputeKey("a"), e);
            }
            return true;
        }

        @Override
        public Set<MessageScope> getMessageScopes(final Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.ORIGINAL;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.NOTHING;
        }
    }
    ////////////////////////////////////////////

    /////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldNotAllowTheSameComputerToExecutedTwice() throws Exception {
        final GraphComputer computer = graphProvider.getGraphComputer(graph).program(new VertexProgramA());
        computer.submit().get(); // this should work as its the first run of the graph computer

        try {
            computer.submit(); // this should fail as the computer has already been executed
            fail("Using the same graph computer to compute again should not be possible");
        } catch (IllegalStateException e) {

        } catch (Exception e) {
            fail("Should yield an illegal state exception for graph computer being executed twice");
        }

        // test no rerun of graph computer
        try {
            computer.submit(); // this should fail as the computer has already been executed even through new program submitted
            fail("Using the same graph computer to compute again should not be possible");
        } catch (IllegalStateException e) {

        } catch (Exception e) {
            fail("Should yield an illegal state exception for graph computer being executed twice");
        }
    }

    public static class VertexProgramA extends StaticVertexProgram {

        @Override
        public void setup(final Memory memory) {

        }

        @Override
        public void execute(final Vertex vertex, final Messenger messenger, final Memory memory) {

        }

        @Override
        public boolean terminate(final Memory memory) {
            return true;
        }

        @Override
        public Set<MessageScope> getMessageScopes(final Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.ORIGINAL;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.NOTHING;
        }
    }
    /////////////////////////////////////////////

    /////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldHaveConsistentMemoryVertexPropertiesAndExceptions() throws Exception {
        ComputerResult results = graphProvider.getGraphComputer(graph).program(new VertexProgramF()).submit().get();
        assertEquals(1, results.memory().getIteration());
        assertEquals(2, results.memory().asMap().size());
        assertEquals(2, results.memory().keys().size());
        assertTrue(results.memory().keys().contains("a"));
        assertTrue(results.memory().keys().contains("b"));
        assertTrue(results.memory().getRuntime() >= 0);

        assertEquals(12, results.memory().<Integer>get("a").intValue());   // 2 iterations
        assertEquals(28, results.memory().<Integer>get("b").intValue());
        try {
            results.memory().get("BAD");
            fail("Should throw an IllegalArgumentException");
        } catch (IllegalArgumentException e) {
            assertEquals(Memory.Exceptions.memoryDoesNotExist("BAD").getMessage(), e.getMessage());
        }
        assertEquals(Long.valueOf(0), results.graph().traversal().V().count().next()); // persist new/nothing.

        results.graph().traversal().V().forEachRemaining(v -> {
            assertTrue(v.property("nameLengthCounter").isPresent());
            assertEquals(Integer.valueOf(v.<String>value("name").length() * 2), Integer.valueOf(v.<Integer>value("nameLengthCounter")));
        });
    }

    public static class VertexProgramF extends StaticVertexProgram<Object> {

        @Override
        public void setup(final Memory memory) {

        }

        @Override
        public void execute(final Vertex vertex, final Messenger<Object> messenger, final Memory memory) {
            try {
                vertex.property(VertexProperty.Cardinality.single, "blah", "blah");
                fail("Should throw an IllegalArgumentException");
            } catch (final IllegalArgumentException e) {
                assertEquals(GraphComputer.Exceptions.providedKeyIsNotAnElementComputeKey("blah").getMessage(), e.getMessage());
            } catch (final Exception e) {
                fail("Should throw an IllegalArgumentException: " + e);
            }

            memory.add("a", 1);
            if (memory.isInitialIteration()) {
                vertex.property(VertexProperty.Cardinality.single, "nameLengthCounter", vertex.<String>value("name").length());
                memory.add("b", vertex.<String>value("name").length());
            } else {
                vertex.property(VertexProperty.Cardinality.single, "nameLengthCounter", vertex.<String>value("name").length() + vertex.<Integer>value("nameLengthCounter"));
            }
        }

        @Override
        public boolean terminate(final Memory memory) {
            return memory.getIteration() == 1;
        }

        @Override
        public Set<VertexComputeKey> getVertexComputeKeys() {
            return Collections.singleton(VertexComputeKey.of("nameLengthCounter", false));
        }

        @Override
        public Set<MemoryComputeKey> getMemoryComputeKeys() {
            return new HashSet<>(Arrays.asList(
                    MemoryComputeKey.of("a", Operator.sum, true, false),
                    MemoryComputeKey.of("b", Operator.sum, true, false)));
        }

        @Override
        public Set<MessageScope> getMessageScopes(Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.NOTHING;
        }
    }
    /////////////////////////////////////////////

    /////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldAndOrIncrCorrectlyThroughSubStages() throws Exception {
        ComputerResult results = graphProvider.getGraphComputer(graph).program(new VertexProgramG()).submit().get();
        assertEquals(2, results.memory().getIteration());
        assertEquals(6, results.memory().asMap().size());
        assertEquals(6, results.memory().keys().size());
        assertTrue(results.memory().keys().contains("a"));
        assertTrue(results.memory().keys().contains("b"));
        assertTrue(results.memory().keys().contains("c"));
        assertTrue(results.memory().keys().contains("d"));
        assertTrue(results.memory().keys().contains("e"));
        assertTrue(results.memory().keys().contains("f"));

        assertEquals(Long.valueOf(18), results.memory().get("a"));
        assertEquals(Long.valueOf(0), results.memory().get("b"));
        assertFalse(results.memory().get("c"));
        assertTrue(results.memory().get("d"));
        assertTrue(results.memory().get("e"));
        assertEquals(3, results.memory().<Integer>get("f").intValue());
    }

    public static class VertexProgramG extends StaticVertexProgram {

        @Override
        public void setup(final Memory memory) {
            memory.set("a", 0l);
            memory.set("b", 0l);
            memory.set("c", true);
            memory.set("d", false);
            memory.set("e", true);
            memory.set("f", memory.getIteration());
            try {
                memory.add("a", 0l);
                fail("Should only allow Memory.set() during VertexProgram.setup()");
            } catch (final Exception e) {
                validateException(Memory.Exceptions.memoryAddOnlyDuringVertexProgramExecute("a"), e);
            }
        }

        @Override
        public void execute(final Vertex vertex, final Messenger messenger, final Memory memory) {
            // test current step values
            assertEquals(Long.valueOf(6 * memory.getIteration()), memory.get("a"));
            assertEquals(Long.valueOf(0), memory.get("b"));
            if (memory.isInitialIteration()) {
                assertTrue(memory.get("c"));
                assertFalse(memory.get("d"));
            } else {
                assertFalse(memory.get("c"));
                assertTrue(memory.get("d"));
            }
            assertTrue(memory.get("e"));
            assertEquals(memory.getIteration(), memory.<Integer>get("f").intValue());

            // update current step values
            memory.add("a", 1l);
            memory.add("b", 1l);
            memory.add("c", false);
            memory.add("d", true);
            memory.add("e", false);
            memory.add("f", memory.getIteration() + 1);

            // test current step values, should be the same as previous prior to update
            assertEquals(Long.valueOf(6 * memory.getIteration()), memory.get("a"));
            assertEquals(Long.valueOf(0), memory.get("b"));
            if (memory.isInitialIteration()) {
                assertTrue(memory.get("c"));
                assertFalse(memory.get("d"));
            } else {
                assertFalse(memory.get("c"));
                assertTrue(memory.get("d"));
            }
            assertTrue(memory.get("e"));
            assertEquals(memory.getIteration(), memory.<Integer>get("f").intValue());
            try {
                memory.set("a", 0l);
                fail("Should only allow Memory.add() during VertexProgram.execute()");
            } catch (final Exception e) {
                validateException(Memory.Exceptions.memorySetOnlyDuringVertexProgramSetUpAndTerminate("a"), e);
            }
        }

        @Override
        public boolean terminate(Memory memory) {
            assertEquals(Long.valueOf(6 * (memory.getIteration() + 1)), memory.get("a"));
            assertEquals(Long.valueOf(6), memory.get("b"));
            assertFalse(memory.get("c"));
            assertTrue(memory.get("d"));
            assertFalse(memory.get("e"));
            assertEquals(memory.getIteration() + 1, memory.<Integer>get("f").intValue());
            memory.set("b", 0l);
            memory.set("e", true);
            try {
                memory.add("a", 0l);
                fail("Should only allow Memory.set() during VertexProgram.terminate()");
            } catch (final Exception e) {
                validateException(Memory.Exceptions.memoryAddOnlyDuringVertexProgramExecute("a"), e);
            }
            return memory.getIteration() > 1;
        }

        @Override
        public Set<MemoryComputeKey> getMemoryComputeKeys() {
            return new HashSet<>(Arrays.asList(
                    MemoryComputeKey.of("a", Operator.sum, true, false),
                    MemoryComputeKey.of("b", Operator.sum, true, false),
                    MemoryComputeKey.of("c", Operator.and, true, false),
                    MemoryComputeKey.of("d", Operator.or, true, false),
                    MemoryComputeKey.of("e", Operator.and, true, false),
                    MemoryComputeKey.of("f", Operator.assign, true, false)));
        }

        @Override
        public Set<MessageScope> getMessageScopes(Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.NOTHING;
        }
    }
    /////////////////////////////////////////////

    /////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldAllowMapReduceWithNoVertexProgram() throws Exception {
        final ComputerResult results = graphProvider.getGraphComputer(graph).mapReduce(new MapReduceA()).submit().get();
        assertEquals(123, results.memory().<Integer>get("ageSum").intValue());
    }

    private static class MapReduceA extends StaticMapReduce<MapReduce.NullObject, Integer, MapReduce.NullObject, Integer, Integer> {

        @Override
        public boolean doStage(final Stage stage) {
            return stage.equals(Stage.MAP) || stage.equals(Stage.REDUCE);
        }

        @Override
        public void map(final Vertex vertex, final MapEmitter<NullObject, Integer> emitter) {
            vertex.<Integer>property("age").ifPresent(emitter::emit);
        }

        @Override
        public void reduce(NullObject key, Iterator<Integer> values, ReduceEmitter<NullObject, Integer> emitter) {
            int sum = 0;
            while (values.hasNext()) {
                sum = sum + values.next();
            }
            emitter.emit(sum);
        }

        @Override
        public Integer generateFinalResult(Iterator<KeyValue<NullObject, Integer>> keyValues) {
            return keyValues.next().getValue();
        }

        @Override
        public String getMemoryKey() {
            return "ageSum";
        }
    }
    /////////////////////////////////////////////

    @Test
    @LoadGraphWith(MODERN)
    public void shouldSupportMultipleMapReduceJobs() throws Exception {
        final ComputerResult results = graphProvider.getGraphComputer(graph)
                .program(new VertexProgramH())
                .mapReduce(new MapReduceH1())
                .mapReduce(new MapReduceH2()).submit().get();
        assertEquals(60, results.memory().<Integer>get("a").intValue());
        assertEquals(1, results.memory().<Integer>get("b").intValue());
    }

    public static class VertexProgramH extends StaticVertexProgram {

        @Override
        public void setup(final Memory memory) {

        }

        @Override
        public void execute(Vertex vertex, Messenger messenger, Memory memory) {
            vertex.property(VertexProperty.Cardinality.single, "counter", memory.isInitialIteration() ? 1 : vertex.<Integer>value("counter") + 1);
        }

        @Override
        public boolean terminate(final Memory memory) {
            return memory.getIteration() > 8;
        }

        @Override
        public Set<VertexComputeKey> getVertexComputeKeys() {
            return Collections.singleton(VertexComputeKey.of("counter", false));
        }

        @Override
        public Set<MessageScope> getMessageScopes(Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.NOTHING;
        }
    }

    private static class MapReduceH1 extends StaticMapReduce<MapReduce.NullObject, Integer, MapReduce.NullObject, Integer, Integer> {

        @Override
        public boolean doStage(final Stage stage) {
            return stage.equals(Stage.MAP) || stage.equals(Stage.REDUCE);
        }

        @Override
        public void map(final Vertex vertex, final MapEmitter<NullObject, Integer> emitter) {
            vertex.<Integer>property("counter").ifPresent(emitter::emit);
        }

        @Override
        public void reduce(final NullObject key, final Iterator<Integer> values, final ReduceEmitter<NullObject, Integer> emitter) {
            int sum = 0;
            while (values.hasNext()) {
                sum = sum + values.next();
            }
            emitter.emit(sum);
        }

        @Override
        public Integer generateFinalResult(final Iterator<KeyValue<NullObject, Integer>> keyValues) {
            return keyValues.next().getValue();
        }

        @Override
        public String getMemoryKey() {
            return "a";
        }
    }

    private static class MapReduceH2 extends StaticMapReduce<Integer, Integer, Integer, Integer, Integer> {

        @Override
        public boolean doStage(final Stage stage) {
            return true;
        }

        @Override
        public void map(final Vertex vertex, final MapEmitter<Integer, Integer> emitter) {
            vertex.<Integer>property("age").ifPresent(age -> emitter.emit(age, age));
        }

        @Override
        public void combine(Integer key, Iterator<Integer> values, ReduceEmitter<Integer, Integer> emitter) {
            values.forEachRemaining(i -> emitter.emit(i, 1));
        }

        @Override
        public void reduce(Integer key, Iterator<Integer> values, ReduceEmitter<Integer, Integer> emitter) {
            values.forEachRemaining(i -> emitter.emit(i, 1));
        }

        @Override
        public Integer generateFinalResult(Iterator<KeyValue<Integer, Integer>> keyValues) {
            return keyValues.next().getValue();
        }

        @Override
        public String getMemoryKey() {
            return "b";
        }
    }
    /////////////////////////////////////////////

    /////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldSortReduceOutput() throws Exception {
        final ComputerResult results = graphProvider.getGraphComputer(graph).mapReduce(new MapReduceB()).submit().get();
        final List<Integer> nameLengths = results.memory().get("nameLengths");
        assertEquals(6, nameLengths.size());
        for (int i = 1; i < nameLengths.size(); i++) {
            assertTrue(nameLengths.get(i) <= nameLengths.get(i - 1));
        }
    }

    public static class MapReduceB extends StaticMapReduce<Integer, Integer, Integer, Integer, List<Integer>> {

        @Override
        public boolean doStage(final Stage stage) {
            return stage.equals(Stage.REDUCE) || stage.equals(Stage.MAP);
        }

        @Override
        public void map(final Vertex vertex, final MapEmitter<Integer, Integer> emitter) {
            emitter.emit(vertex.<String>value("name").length(), vertex.<String>value("name").length());
        }

        @Override
        public void reduce(Integer key, Iterator<Integer> values, ReduceEmitter<Integer, Integer> emitter) {
            values.forEachRemaining(id -> emitter.emit(id, id));
        }

        @Override
        public Optional<Comparator<Integer>> getReduceKeySort() {
            return Optional.of(Comparator.<Integer>reverseOrder());
        }

        @Override
        public String getMemoryKey() {
            return "nameLengths";
        }

        @Override
        public List<Integer> generateFinalResult(final Iterator<KeyValue<Integer, Integer>> keyValues) {
            final List<Integer> list = new ArrayList<>();
            keyValues.forEachRemaining(nameLength -> list.add(nameLength.getKey()));
            return list;
        }
    }

    /////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldSortMapOutput() throws Exception {
        final ComputerResult results = graphProvider.getGraphComputer(graph).mapReduce(new MapReduceBB()).submit().get();
        final List<Integer> nameLengths = results.memory().get("nameLengths");
        assertEquals(6, nameLengths.size());
        for (int i = 1; i < nameLengths.size(); i++) {
            assertTrue(nameLengths.get(i) <= nameLengths.get(i - 1));
        }
    }

    public static class MapReduceBB extends StaticMapReduce<Integer, Integer, Integer, Integer, List<Integer>> {

        @Override
        public boolean doStage(final Stage stage) {
            return stage.equals(Stage.MAP);
        }

        @Override
        public void map(final Vertex vertex, final MapEmitter<Integer, Integer> emitter) {
            emitter.emit(vertex.<String>value("name").length(), vertex.<String>value("name").length());
        }

        @Override
        public Optional<Comparator<Integer>> getMapKeySort() {
            return Optional.of(Comparator.<Integer>reverseOrder());
        }

        @Override
        public String getMemoryKey() {
            return "nameLengths";
        }

        @Override
        public List<Integer> generateFinalResult(final Iterator<KeyValue<Integer, Integer>> keyValues) {
            final List<Integer> list = new ArrayList<>();
            keyValues.forEachRemaining(nameLength -> list.add(nameLength.getKey()));
            return list;
        }
    }


    /////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldOnlyAllowReadingVertexPropertiesInMapReduce() throws Exception {
        graphProvider.getGraphComputer(graph).mapReduce(new MapReduceC()).submit().get();
    }

    public static class MapReduceC extends StaticMapReduce<MapReduce.NullObject, MapReduce.NullObject, MapReduce.NullObject, MapReduce.NullObject, MapReduce.NullObject> {

        @Override
        public boolean doStage(final Stage stage) {
            return stage.equals(Stage.MAP);
        }

        @Override
        public void map(final Vertex vertex, final MapEmitter<MapReduce.NullObject, MapReduce.NullObject> emitter) {
            try {
                vertex.edges(Direction.OUT);
                fail("Edges should not be accessible in MapReduce.map()");
            } catch (final UnsupportedOperationException e) {
                assertEquals(GraphComputer.Exceptions.incidentAndAdjacentElementsCanNotBeAccessedInMapReduce().getMessage(), e.getMessage());
            }
            try {
                vertex.edges(Direction.IN);
                fail("Edges should not be accessible in MapReduce.map()");
            } catch (final UnsupportedOperationException e) {
                assertEquals(GraphComputer.Exceptions.incidentAndAdjacentElementsCanNotBeAccessedInMapReduce().getMessage(), e.getMessage());
            }
            try {
                vertex.edges(Direction.BOTH);
                fail("Edges should not be accessible in MapReduce.map()");
            } catch (final UnsupportedOperationException e) {
                assertEquals(GraphComputer.Exceptions.incidentAndAdjacentElementsCanNotBeAccessedInMapReduce().getMessage(), e.getMessage());
            }
            ////
            try {
                vertex.property("name", "bob");
                fail("Vertex properties should be immutable in MapReduce.map()");
            } catch (final UnsupportedOperationException e) {
                assertEquals(GraphComputer.Exceptions.vertexPropertiesCanNotBeUpdatedInMapReduce().getMessage(), e.getMessage());
            }
            try {
                vertex.property("name").property("test", 1);
                fail("Vertex properties should be immutable in MapReduce.map()");
            } catch (final UnsupportedOperationException e) {
                assertEquals(GraphComputer.Exceptions.vertexPropertiesCanNotBeUpdatedInMapReduce().getMessage(), e.getMessage());
            }

        }

        @Override
        public String getMemoryKey() {
            return "nothing";
        }

        @Override
        public MapReduce.NullObject generateFinalResult(final Iterator<KeyValue<MapReduce.NullObject, MapReduce.NullObject>> keyValues) {
            return MapReduce.NullObject.instance();
        }
    }
    /////////////////////////////////////////////

    /////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldOnlyAllowIDAccessOfAdjacentVertices() throws Exception {
        graphProvider.getGraphComputer(graph).program(new VertexProgramI()).submit().get();
    }

    public static class VertexProgramI extends StaticVertexProgram<MapReduce.NullObject> {

        @Override
        public void setup(final Memory memory) {

        }

        @Override
        public void execute(Vertex vertex, Messenger messenger, Memory memory) {
            vertex.vertices(Direction.OUT).forEachRemaining(Vertex::id);
            vertex.vertices(Direction.IN).forEachRemaining(Vertex::id);
            vertex.vertices(Direction.BOTH).forEachRemaining(Vertex::id);
            if (vertex.vertices(Direction.OUT).hasNext()) {
                try {
                    vertex.vertices(Direction.OUT).forEachRemaining(Vertex::label);
                    fail("Adjacent vertex labels should not be accessible in VertexProgram.execute()");
                } catch (UnsupportedOperationException e) {
                    assertEquals(GraphComputer.Exceptions.adjacentVertexLabelsCanNotBeRead().getMessage(), e.getMessage());
                }
            }
            if (vertex.vertices(Direction.IN).hasNext()) {
                try {
                    vertex.vertices(Direction.IN).forEachRemaining(Vertex::label);
                    fail("Adjacent vertex labels should not be accessible in VertexProgram.execute()");
                } catch (UnsupportedOperationException e) {
                    assertEquals(GraphComputer.Exceptions.adjacentVertexLabelsCanNotBeRead().getMessage(), e.getMessage());
                }
            }
            if (vertex.vertices(Direction.BOTH).hasNext()) {
                try {
                    vertex.vertices(Direction.BOTH).forEachRemaining(Vertex::label);
                    fail("Adjacent vertex labels should not be accessible in VertexProgram.execute()");
                } catch (UnsupportedOperationException e) {
                    assertEquals(GraphComputer.Exceptions.adjacentVertexLabelsCanNotBeRead().getMessage(), e.getMessage());
                }
            }
            ////////////////////
            if (vertex.vertices(Direction.OUT).hasNext()) {
                try {
                    vertex.vertices(Direction.OUT).forEachRemaining(v -> v.property("name"));
                    fail("Adjacent vertex properties should not be accessible in VertexProgram.execute()");
                } catch (UnsupportedOperationException e) {
                    assertEquals(GraphComputer.Exceptions.adjacentVertexPropertiesCanNotBeReadOrUpdated().getMessage(), e.getMessage());
                }
            }
            if (vertex.vertices(Direction.IN).hasNext()) {
                try {
                    vertex.vertices(Direction.IN).forEachRemaining(v -> v.property("name"));
                    fail("Adjacent vertex properties should not be accessible in VertexProgram.execute()");
                } catch (UnsupportedOperationException e) {
                    assertEquals(GraphComputer.Exceptions.adjacentVertexPropertiesCanNotBeReadOrUpdated().getMessage(), e.getMessage());
                }
            }
            if (vertex.vertices(Direction.BOTH).hasNext()) {
                try {
                    vertex.vertices(Direction.BOTH).forEachRemaining(v -> v.property("name"));
                    fail("Adjacent vertex properties should not be accessible in VertexProgram.execute()");
                } catch (UnsupportedOperationException e) {
                    assertEquals(GraphComputer.Exceptions.adjacentVertexPropertiesCanNotBeReadOrUpdated().getMessage(), e.getMessage());
                }
            }
            ////////////////////
            if (vertex.vertices(Direction.BOTH).hasNext()) {
                try {
                    vertex.vertices(Direction.BOTH).forEachRemaining(v -> v.edges(Direction.BOTH));
                    fail("Adjacent vertex edges should not be accessible in VertexProgram.execute()");
                } catch (UnsupportedOperationException e) {
                    assertEquals(GraphComputer.Exceptions.adjacentVertexEdgesAndVerticesCanNotBeReadOrUpdated().getMessage(), e.getMessage());
                }
            }
            if (vertex.vertices(Direction.BOTH).hasNext()) {
                try {
                    vertex.vertices(Direction.BOTH).forEachRemaining(v -> v.vertices(Direction.BOTH));
                    fail("Adjacent vertex vertices should not be accessible in VertexProgram.execute()");
                } catch (UnsupportedOperationException e) {
                    assertEquals(GraphComputer.Exceptions.adjacentVertexEdgesAndVerticesCanNotBeReadOrUpdated().getMessage(), e.getMessage());
                }
            }

        }

        @Override
        public boolean terminate(final Memory memory) {
            return memory.getIteration() > 1;
        }

        @Override
        public Set<MessageScope> getMessageScopes(Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.NOTHING;
        }
    }
    /////////////////////////////////////////////

    /////////////////////////////////////////////

    @Test
    @LoadGraphWith(MODERN)
    public void shouldStartAndEndWorkersForVertexProgramAndMapReduce() throws Exception {
        MapReduceI.WORKER_START.clear();
        MapReduceI.WORKER_END.clear();
        assertEquals(3, graphProvider.getGraphComputer(graph).program(new VertexProgramJ()).mapReduce(new MapReduceI()).submit().get().memory().<Integer>get("a").intValue());
        if (MapReduceI.WORKER_START.size() == 2) {
            assertEquals(2, MapReduceI.countTrue(MapReduceI.WORKER_START));
            assertTrue(MapReduceI.WORKER_START.contains(MapReduce.Stage.MAP) && MapReduceI.WORKER_START.contains(MapReduce.Stage.REDUCE));
        } else {
            assertEquals(3, MapReduceI.countTrue(MapReduceI.WORKER_START));
            assertTrue(MapReduceI.WORKER_START.contains(MapReduce.Stage.MAP) && MapReduceI.WORKER_START.contains(MapReduce.Stage.COMBINE) && MapReduceI.WORKER_START.contains(MapReduce.Stage.REDUCE));
        }
        if (MapReduceI.WORKER_END.size() == 2) {
            assertEquals(2, MapReduceI.countTrue(MapReduceI.WORKER_END));
            assertTrue(MapReduceI.WORKER_END.contains(MapReduce.Stage.MAP) && MapReduceI.WORKER_END.contains(MapReduce.Stage.REDUCE));
        } else {
            assertEquals(3, MapReduceI.countTrue(MapReduceI.WORKER_END));
            assertTrue(MapReduceI.WORKER_END.contains(MapReduce.Stage.MAP) && MapReduceI.WORKER_END.contains(MapReduce.Stage.COMBINE) && MapReduceI.WORKER_END.contains(MapReduce.Stage.REDUCE));
        }
    }

    public static class VertexProgramJ extends StaticVertexProgram {


        @Override
        public void setup(final Memory memory) {
            memory.set("test", memory.getIteration());
        }

        @Override
        public void workerIterationStart(final Memory memory) {
            assertEquals(memory.getIteration() * 6, memory.<Integer>get("test").intValue());
            try {
                memory.add("test", memory.getIteration());
                fail("Should throw an immutable memory exception");
            } catch (IllegalStateException e) {
                assertEquals(Memory.Exceptions.memoryIsCurrentlyImmutable().getMessage(), e.getMessage());
            }
        }

        @Override
        public void execute(Vertex vertex, Messenger messenger, Memory memory) {
            assertEquals(memory.getIteration() * 6, memory.<Integer>get("test").intValue());
            memory.add("test", 1);
        }

        @Override
        public boolean terminate(final Memory memory) {
            return memory.getIteration() > 3;
        }

        @Override
        public void workerIterationEnd(final Memory memory) {
            assertEquals(memory.getIteration() * 6, memory.<Integer>get("test").intValue());
            try {
                memory.set("test", memory.getIteration());
                fail("Should throw an immutable memory exception");
            } catch (IllegalStateException e) {
                assertEquals(Memory.Exceptions.memoryIsCurrentlyImmutable().getMessage(), e.getMessage());
            }
        }

        @Override
        public Set<MemoryComputeKey> getMemoryComputeKeys() {
            return Collections.singleton(MemoryComputeKey.of("test", Operator.sum, true, false));
        }

        @Override
        public Set<MessageScope> getMessageScopes(Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.NOTHING;
        }
    }

    private static class MapReduceI extends StaticMapReduce<MapReduce.NullObject, Integer, MapReduce.NullObject, Integer, Integer> {

        private static final Set<Stage> WORKER_START = new ConcurrentSkipListSet<>();
        private static final Set<Stage> WORKER_END = new ConcurrentSkipListSet<>();

        @Override
        public boolean doStage(final Stage stage) {
            return true;
        }

        @Override
        public void workerStart(final Stage stage) {
            WORKER_START.add(stage);
            if (!stage.equals(Stage.MAP))
                assertFalse(WORKER_END.isEmpty());
        }

        @Override
        public void map(final Vertex vertex, final MapEmitter<NullObject, Integer> emitter) {
            emitter.emit(1);
            assertEquals(1, countTrue(WORKER_START));
            assertTrue(WORKER_START.contains(Stage.MAP));
        }

        @Override
        public void combine(final NullObject key, final Iterator<Integer> values, final ReduceEmitter<NullObject, Integer> emitter) {
            emitter.emit(2);
            assertEquals(2, countTrue(WORKER_START));
            assertTrue(WORKER_START.contains(Stage.MAP) && WORKER_START.contains(Stage.COMBINE));
            assertFalse(WORKER_END.isEmpty());
        }

        @Override
        public void reduce(final NullObject key, final Iterator<Integer> values, final ReduceEmitter<NullObject, Integer> emitter) {
            emitter.emit(3);
            if (IteratorUtils.count(WORKER_START) == 2) {
                assertEquals(2, countTrue(WORKER_START));
                assertTrue(WORKER_START.contains(Stage.MAP) && WORKER_START.contains(Stage.REDUCE));
            } else {
                assertEquals(3, countTrue(WORKER_START));
                assertTrue(WORKER_START.contains(Stage.MAP) && WORKER_START.contains(Stage.COMBINE) && WORKER_START.contains(Stage.REDUCE));
            }
            assertFalse(WORKER_END.isEmpty());
        }

        @Override
        public void workerEnd(final Stage stage) {
            assertFalse(WORKER_START.isEmpty());
            if (!stage.equals(Stage.MAP))
                assertFalse(WORKER_END.isEmpty());
            WORKER_END.add(stage);
        }

        @Override
        public Integer generateFinalResult(final Iterator<KeyValue<NullObject, Integer>> keyValues) {
            assertEquals(3, keyValues.next().getValue().intValue());
            return 3;
        }

        @Override
        public String getMemoryKey() {
            return "a";
        }

        /**
         * Javadoc for {@code ConcurrentSkipListSet} includes this little note: "Beware that, unlike in most
         * collections, the size method is not a constant-time operation." Doing a traversal of elements will yield
         * the correct count. Java 11 upgrade seemed to force this failure consistently.
         */
        static int countTrue(final Set<Stage> s) {
            return Long.valueOf(IteratorUtils.count(s)).intValue();
        }
    }

    /////////////////////////////////////////////

    /////////////////////////////////////////////
    @Test
    @LoadGraphWith
    public void shouldSupportPersistResultGraphPairsStatedInFeatures() throws Exception {
        for (final GraphComputer.ResultGraph resultGraph : GraphComputer.ResultGraph.values()) {
            for (final GraphComputer.Persist persist : GraphComputer.Persist.values()) {
                final GraphComputer computer = graphProvider.getGraphComputer(graph);
                if (computer.features().supportsResultGraphPersistCombination(resultGraph, persist)) {
                    computer.program(new VertexProgramK()).result(resultGraph).persist(persist).submit().get();
                } else {
                    try {
                        computer.program(new VertexProgramK()).result(resultGraph).persist(persist).submit().get();
                        fail("The GraphComputer " + computer + " states that it does support the following resultGraph/persist pair: " + resultGraph + ":" + persist);
                    } catch (final IllegalArgumentException e) {
                        assertEquals(GraphComputer.Exceptions.resultGraphPersistCombinationNotSupported(resultGraph, persist).getMessage(), e.getMessage());
                    }
                }
            }
        }
    }

    @Test
    @LoadGraphWith(MODERN)
    public void shouldProcessResultGraphNewWithPersistNothing() throws Exception {
        final GraphComputer computer = graphProvider.getGraphComputer(graph);
        if (computer.features().supportsResultGraphPersistCombination(GraphComputer.ResultGraph.NEW, GraphComputer.Persist.NOTHING)) {
            final ComputerResult result = computer.program(new VertexProgramK()).result(GraphComputer.ResultGraph.NEW).persist(GraphComputer.Persist.NOTHING).submit().get();
            assertEquals(Long.valueOf(0l), result.graph().traversal().V().count().next());
            assertEquals(Long.valueOf(0l), result.graph().traversal().E().count().next());
            assertEquals(Long.valueOf(0l), result.graph().traversal().V().values().count().next());
            assertEquals(Long.valueOf(0l), result.graph().traversal().E().values().count().next());
            assertFalse(result.graph().traversal().V().values("money").sum().hasNext());
            ///
            assertEquals(Long.valueOf(6l), graph.traversal().V().count().next());
            assertEquals(Long.valueOf(6l), graph.traversal().E().count().next());
            assertEquals(Long.valueOf(12l), graph.traversal().V().values().count().next());
            assertEquals(Long.valueOf(6l), graph.traversal().E().values().count().next());
            assertFalse(graph.traversal().V().values("money").sum().hasNext());
        }
    }

    @Test
    @LoadGraphWith(MODERN)
    public void shouldProcessResultGraphNewWithPersistVertexProperties() throws Exception {
        final GraphComputer computer = graphProvider.getGraphComputer(graph);
        if (computer.features().supportsResultGraphPersistCombination(GraphComputer.ResultGraph.NEW, GraphComputer.Persist.VERTEX_PROPERTIES)) {
            final ComputerResult result = computer.program(new VertexProgramK()).result(GraphComputer.ResultGraph.NEW).persist(GraphComputer.Persist.VERTEX_PROPERTIES).submit().get();
            assertEquals(Long.valueOf(6l), result.graph().traversal().V().count().next());
            assertEquals(Long.valueOf(0l), result.graph().traversal().E().count().next());
            assertEquals(Long.valueOf(18l), result.graph().traversal().V().values().count().next());
            assertEquals(Long.valueOf(0l), result.graph().traversal().E().values().count().next());
            assertEquals(28l, result.graph().traversal().V().values("money").sum().next());
            ///
            assertEquals(Long.valueOf(6l), graph.traversal().V().count().next());
            assertEquals(Long.valueOf(6l), graph.traversal().E().count().next());
            assertEquals(Long.valueOf(12l), graph.traversal().V().values().count().next());
            assertEquals(Long.valueOf(6l), graph.traversal().E().values().count().next());
            assertFalse(graph.traversal().V().values("money").sum().hasNext());
        }
    }

    @Test
    @LoadGraphWith(MODERN)
    public void shouldProcessResultGraphNewWithPersistEdges() throws Exception {
        final GraphComputer computer = graphProvider.getGraphComputer(graph);
        if (computer.features().supportsResultGraphPersistCombination(GraphComputer.ResultGraph.NEW, GraphComputer.Persist.EDGES)) {
            final ComputerResult result = computer.program(new VertexProgramK()).result(GraphComputer.ResultGraph.NEW).persist(GraphComputer.Persist.EDGES).submit().get();
            assertEquals(Long.valueOf(6l), result.graph().traversal().V().count().next());
            assertEquals(Long.valueOf(6l), result.graph().traversal().E().count().next());
            assertEquals(Long.valueOf(18l), result.graph().traversal().V().values().count().next());
            assertEquals(Long.valueOf(6l), result.graph().traversal().E().values().count().next());
            assertEquals(28l, result.graph().traversal().V().values("money").sum().next());
            ///
            assertEquals(Long.valueOf(6l), graph.traversal().V().count().next());
            assertEquals(Long.valueOf(6l), graph.traversal().E().count().next());
            assertEquals(Long.valueOf(12l), graph.traversal().V().values().count().next());
            assertEquals(Long.valueOf(6l), graph.traversal().E().values().count().next());
            assertFalse(graph.traversal().V().values("money").sum().hasNext());
        }
    }

    @Test
    @LoadGraphWith(MODERN)
    public void shouldProcessResultGraphOriginalWithPersistNothing() throws Exception {
        final GraphComputer computer = graphProvider.getGraphComputer(graph);
        if (computer.features().supportsResultGraphPersistCombination(GraphComputer.ResultGraph.ORIGINAL, GraphComputer.Persist.NOTHING)) {
            final ComputerResult result = computer.program(new VertexProgramK()).result(GraphComputer.ResultGraph.ORIGINAL).persist(GraphComputer.Persist.NOTHING).submit().get();
            assertEquals(Long.valueOf(6l), result.graph().traversal().V().count().next());
            assertEquals(Long.valueOf(6l), result.graph().traversal().E().count().next());
            assertEquals(Long.valueOf(12l), result.graph().traversal().V().values().count().next());
            assertEquals(Long.valueOf(6l), result.graph().traversal().E().values().count().next());
            assertFalse(result.graph().traversal().V().values("money").sum().hasNext());
            ///
            assertEquals(Long.valueOf(6l), graph.traversal().V().count().next());
            assertEquals(Long.valueOf(6l), graph.traversal().E().count().next());
            assertEquals(Long.valueOf(12l), graph.traversal().V().values().count().next());
            assertEquals(Long.valueOf(6l), graph.traversal().E().values().count().next());
            assertFalse(graph.traversal().V().values("money").sum().hasNext());
        }
    }

    @Test
    @LoadGraphWith(MODERN)
    public void shouldProcessResultGraphOriginalWithPersistVertexProperties() throws Exception {
        final GraphComputer computer = graphProvider.getGraphComputer(graph);
        if (computer.features().supportsResultGraphPersistCombination(GraphComputer.ResultGraph.ORIGINAL, GraphComputer.Persist.VERTEX_PROPERTIES)) {
            final ComputerResult result = computer.program(new VertexProgramK()).result(GraphComputer.ResultGraph.ORIGINAL).persist(GraphComputer.Persist.VERTEX_PROPERTIES).submit().get();
            assertEquals(Long.valueOf(6l), result.graph().traversal().V().count().next());
            assertEquals(Long.valueOf(6l), result.graph().traversal().E().count().next());
            assertEquals(Long.valueOf(18l), result.graph().traversal().V().values().count().next());
            assertEquals(Long.valueOf(6l), result.graph().traversal().E().values().count().next());
            assertEquals(28l, result.graph().traversal().V().values("money").sum().next());
            ///
            assertEquals(Long.valueOf(6l), graph.traversal().V().count().next());
            assertEquals(Long.valueOf(6l), graph.traversal().E().count().next());
            assertEquals(Long.valueOf(18l), graph.traversal().V().values().count().next());
            assertEquals(Long.valueOf(6l), graph.traversal().E().values().count().next());
            assertEquals(28l, graph.traversal().V().values("money").sum().next());
        }
    }

    @Test
    @LoadGraphWith(MODERN)
    public void shouldProcessResultGraphOriginalWithPersistEdges() throws Exception {
        final GraphComputer computer = graphProvider.getGraphComputer(graph);
        if (computer.features().supportsResultGraphPersistCombination(GraphComputer.ResultGraph.ORIGINAL, GraphComputer.Persist.EDGES)) {
            final ComputerResult result = computer.program(new VertexProgramK()).result(GraphComputer.ResultGraph.ORIGINAL).persist(GraphComputer.Persist.EDGES).submit().get();
            assertEquals(Long.valueOf(6l), result.graph().traversal().V().count().next());
            assertEquals(Long.valueOf(6l), result.graph().traversal().E().count().next());
            assertEquals(Long.valueOf(18l), result.graph().traversal().V().values().count().next());
            assertEquals(Long.valueOf(6l), result.graph().traversal().E().values().count().next());
            assertEquals(28l, result.graph().traversal().V().values("money").sum().next());
            ///
            assertEquals(Long.valueOf(6l), graph.traversal().V().count().next());
            assertEquals(Long.valueOf(6l), graph.traversal().E().count().next());
            assertEquals(Long.valueOf(18l), graph.traversal().V().values().count().next());
            assertEquals(Long.valueOf(6l), graph.traversal().E().values().count().next());
            assertEquals(28l, graph.traversal().V().values("money").sum().next());
        }
    }

    public static class VertexProgramK extends StaticVertexProgram {


        @Override
        public void setup(final Memory memory) {

        }

        @Override
        public void execute(final Vertex vertex, final Messenger messenger, final Memory memory) {
            vertex.property("money", vertex.<String>value("name").length());
        }

        @Override
        public boolean terminate(final Memory memory) {
            return true;
        }

        @Override
        public Set<VertexComputeKey> getVertexComputeKeys() {
            return Collections.singleton(VertexComputeKey.of("money", false));
        }

        @Override
        public Set<MessageScope> getMessageScopes(Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.EDGES;
        }
    }

    /////////////////////////////////////////////

    @Test
    @LoadGraphWith(GRATEFUL)
    public void shouldSupportWorkerCount() throws Exception {
        int maxWorkers = graphProvider.getGraphComputer(graph).features().getMaxWorkers();
        if (maxWorkers != Integer.MAX_VALUE) {
            for (int i = maxWorkers + 1; i < maxWorkers + 10; i++) {
                try {
                    graphProvider.getGraphComputer(graph).program(new VertexProgramL()).workers(i).submit().get();
                    fail("Should throw a GraphComputer.Exceptions.computerRequiresMoreWorkersThanSupported() exception");
                } catch (final IllegalArgumentException e) {
                    assertTrue(e.getMessage().contains("computer requires more workers"));
                }
            }
        }
        if (maxWorkers > 25) maxWorkers = 25;
        for (int i = 1; i <= maxWorkers; i++) {
            ComputerResult result = graphProvider.getGraphComputer(graph).program(new VertexProgramL()).workers(i).submit().get();
            assertEquals(Integer.valueOf(i).longValue(), (long) result.memory().get("workerCount"));
        }
    }

    public static class VertexProgramL implements VertexProgram {

        boolean announced = false;

        @Override
        public void setup(final Memory memory) {
            memory.set("workerCount", 0l);
        }

        @Override
        public void execute(final Vertex vertex, final Messenger messenger, final Memory memory) {
            try {
                Thread.sleep(1);
            } catch (Exception e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            if (!this.announced) {
                memory.add("workerCount", 1l);
                this.announced = true;
            }
        }

        @Override
        public boolean terminate(final Memory memory) {
            return true;
        }

        @Override
        public Set<MemoryComputeKey> getMemoryComputeKeys() {
            return Collections.singleton(MemoryComputeKey.of("workerCount", Operator.sum, true, false));
        }

        /*public void workerIterationStart(final Memory memory) {
            assertEquals(0l, (long) memory.get("workerCount"));
        }

        public void workerIterationEnd(final Memory memory) {
            assertEquals(1l, (long) memory.get("workerCount"));
        }*/

        @Override
        public Set<MessageScope> getMessageScopes(Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.NOTHING;
        }

        @Override
        @SuppressWarnings("CloneDoesntCallSuperClone,CloneDoesntDeclareCloneNotSupportedException")
        public VertexProgramL clone() {
            return new VertexProgramL();
        }

        @Override
        public void storeState(final Configuration configuration) {
            VertexProgram.super.storeState(configuration);
        }
    }

    /////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldSupportMultipleScopes() throws ExecutionException, InterruptedException {
        final ComputerResult result = graphProvider.getGraphComputer(graph).program(new MultiScopeVertexProgram()).submit().get();
        assertEquals(result.graph().traversal().V().has("name", "josh").next().property(MultiScopeVertexProgram.MEMORY_KEY).value(), 0L);
        assertEquals(result.graph().traversal().V().has("name", "lop").next().property(MultiScopeVertexProgram.MEMORY_KEY).value(), 1L);
        assertEquals(result.graph().traversal().V().has("name", "ripple").next().property(MultiScopeVertexProgram.MEMORY_KEY).value(), 1L);
        assertEquals(result.graph().traversal().V().has("name", "marko").next().property(MultiScopeVertexProgram.MEMORY_KEY).value(), 2L);
    }

    public static class MultiScopeVertexProgram extends StaticVertexProgram<Long> {

        private final MessageScope.Local<Long> countMessageScopeIn = MessageScope.Local.of(__::inE);
        private final MessageScope.Local<Long> countMessageScopeOut = MessageScope.Local.of(__::outE);

        private static final String MEMORY_KEY = "count";


        @Override
        public void setup(final Memory memory) {
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.VERTEX_PROPERTIES;
        }

        @Override
        public Set<VertexComputeKey> getVertexComputeKeys() {
            return Collections.singleton(VertexComputeKey.of(MEMORY_KEY, false));
        }

        @Override
        public Set<MessageScope> getMessageScopes(final Memory memory) {
            HashSet<MessageScope> scopes = new HashSet<>();
            scopes.add(countMessageScopeIn);
            scopes.add(countMessageScopeOut);
            return scopes;
        }

        @Override
        public void execute(Vertex vertex, Messenger<Long> messenger, Memory memory) {
            switch (memory.getIteration()) {
                case 0:
                    if (vertex.value("name").equals("josh")) {
                        messenger.sendMessage(this.countMessageScopeIn, 2L);
                        messenger.sendMessage(this.countMessageScopeOut, 1L);
                    }
                    break;
                case 1:
                    long edgeCount = IteratorUtils.reduce(messenger.receiveMessages(), 0L, (a, b) -> a + b);
                    vertex.property(MEMORY_KEY, edgeCount);
                    break;
            }
        }

        @Override
        public boolean terminate(final Memory memory) {
            return memory.getIteration() == 1;
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }
    }

    /////////////////////////////////////////////

    /////////////////////////////////////////////
    @Test
    @LoadGraphWith(MODERN)
    public void shouldSupportMultipleScopesWithEdgeFunction() throws ExecutionException, InterruptedException {
        final ComputerResult result = graphProvider.getGraphComputer(graph).program(new MultiScopeVertexWithEdgeFunctionProgram()).submit().get();
        assertEquals(result.graph().traversal().V().has("name", "josh").next().property(MultiScopeVertexProgram.MEMORY_KEY).value(), 0L);
        assertEquals(result.graph().traversal().V().has("name", "lop").next().property(MultiScopeVertexProgram.MEMORY_KEY).value(), 4L);
        assertEquals(result.graph().traversal().V().has("name", "ripple").next().property(MultiScopeVertexProgram.MEMORY_KEY).value(), 10L);
        assertEquals(result.graph().traversal().V().has("name", "marko").next().property(MultiScopeVertexProgram.MEMORY_KEY).value(), 20L);
    }

    public static class MultiScopeVertexWithEdgeFunctionProgram extends StaticVertexProgram<Long> {

        private final MessageScope.Local<Long> countMessageScopeIn = MessageScope.Local.of(__::inE, (m,e) -> m * Math.round(((Double) e.values("weight").next()) * 10));
        private final MessageScope.Local<Long> countMessageScopeOut = MessageScope.Local.of(__::outE, (m,e) -> m * Math.round(((Double) e.values("weight").next()) * 10));

        private static final String MEMORY_KEY = "count";


        @Override
        public void setup(final Memory memory) {
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.VERTEX_PROPERTIES;
        }

        @Override
        public Set<VertexComputeKey> getVertexComputeKeys() {
            return Collections.singleton(VertexComputeKey.of(MEMORY_KEY, false));
        }

        @Override
        public Set<MessageScope> getMessageScopes(final Memory memory) {
            HashSet<MessageScope> scopes = new HashSet<>();
            scopes.add(countMessageScopeIn);
            scopes.add(countMessageScopeOut);
            return scopes;
        }

        @Override
        public void execute(Vertex vertex, Messenger<Long> messenger, Memory memory) {
            switch (memory.getIteration()) {
                case 0:
                    if (vertex.value("name").equals("josh")) {
                        messenger.sendMessage(this.countMessageScopeIn, 2L);
                        messenger.sendMessage(this.countMessageScopeOut, 1L);
                    }
                    break;
                case 1:
                    long edgeCount = IteratorUtils.reduce(messenger.receiveMessages(), 0L, (a, b) -> a + b);
                    vertex.property(MEMORY_KEY, edgeCount);
                    break;
            }
        }

        @Override
        public boolean terminate(final Memory memory) {
            return memory.getIteration() == 1;
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }
    }

    /////////////////////////////////////////////

    @Test
    @LoadGraphWith(MODERN)
    public void shouldSupportGraphFilter() throws Exception {
        // if the graph computer does not support graph filter, then make sure its exception handling is correct
        if (!graphProvider.getGraphComputer(graph).features().supportsGraphFilter()) {
            try {
                graphProvider.getGraphComputer(graph).vertices(__.hasLabel("software"));
                fail("Should throw an unsupported operation exception");
            } catch (final UnsupportedOperationException e) {
                assertEquals(GraphComputer.Exceptions.graphFilterNotSupported().getMessage(), e.getMessage());
            }
            try {
                graphProvider.getGraphComputer(graph).edges(__.<Vertex>outE().limit(10));
                fail("Should throw an unsupported operation exception");
            } catch (final UnsupportedOperationException e) {
                assertEquals(GraphComputer.Exceptions.graphFilterNotSupported().getMessage(), e.getMessage());
            }
            return;
        }
        /// VERTEX PROGRAM
        graphProvider.getGraphComputer(graph).vertices(__.hasLabel("software")).program(new VertexProgramM(VertexProgramM.SOFTWARE_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).vertices(__.hasLabel("person")).program(new VertexProgramM(VertexProgramM.PEOPLE_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).edges(__.bothE("knows")).program(new VertexProgramM(VertexProgramM.KNOWS_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).vertices(__.hasLabel("person")).edges(__.bothE("knows")).program(new VertexProgramM(VertexProgramM.PEOPLE_KNOWS_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).vertices(__.hasLabel("person")).edges(__.<Vertex>bothE("knows").has("weight", P.gt(0.5f))).program(new VertexProgramM(VertexProgramM.PEOPLE_KNOWS_WELL_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).edges(__.<Vertex>bothE().limit(0)).program(new VertexProgramM(VertexProgramM.VERTICES_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).edges(__.<Vertex>outE().limit(1)).program(new VertexProgramM(VertexProgramM.ONE_OUT_EDGE_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).edges(outE()).program(new VertexProgramM(VertexProgramM.OUT_EDGES_ONLY)).submit().get();

        /// VERTEX PROGRAM + MAP REDUCE
        graphProvider.getGraphComputer(graph).vertices(__.hasLabel("software")).program(new VertexProgramM(VertexProgramM.SOFTWARE_ONLY)).mapReduce(new MapReduceJ(VertexProgramM.SOFTWARE_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).vertices(__.hasLabel("person")).program(new VertexProgramM(VertexProgramM.PEOPLE_ONLY)).mapReduce(new MapReduceJ(VertexProgramM.PEOPLE_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).edges(__.bothE("knows")).program(new VertexProgramM(VertexProgramM.KNOWS_ONLY)).mapReduce(new MapReduceJ(VertexProgramM.KNOWS_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).vertices(__.hasLabel("person")).edges(__.bothE("knows")).program(new VertexProgramM(VertexProgramM.PEOPLE_KNOWS_ONLY)).mapReduce(new MapReduceJ(VertexProgramM.PEOPLE_KNOWS_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).vertices(__.hasLabel("person")).edges(__.<Vertex>bothE("knows").has("weight", P.gt(0.5f))).program(new VertexProgramM(VertexProgramM.PEOPLE_KNOWS_WELL_ONLY)).mapReduce(new MapReduceJ(VertexProgramM.PEOPLE_KNOWS_WELL_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).edges(__.<Vertex>bothE().limit(0)).program(new VertexProgramM(VertexProgramM.VERTICES_ONLY)).mapReduce(new MapReduceJ(VertexProgramM.VERTICES_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).edges(__.<Vertex>outE().limit(1)).program(new VertexProgramM(VertexProgramM.ONE_OUT_EDGE_ONLY)).mapReduce(new MapReduceJ(VertexProgramM.ONE_OUT_EDGE_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).edges(outE()).program(new VertexProgramM(VertexProgramM.OUT_EDGES_ONLY)).mapReduce(new MapReduceJ(VertexProgramM.OUT_EDGES_ONLY)).submit().get();

        /// MAP REDUCE ONLY
        graphProvider.getGraphComputer(graph).vertices(__.hasLabel("software")).mapReduce(new MapReduceJ(VertexProgramM.SOFTWARE_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).vertices(__.hasLabel("person")).mapReduce(new MapReduceJ(VertexProgramM.PEOPLE_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).edges(__.bothE("knows")).mapReduce(new MapReduceJ(VertexProgramM.KNOWS_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).vertices(__.hasLabel("person")).edges(__.bothE("knows")).mapReduce(new MapReduceJ(VertexProgramM.PEOPLE_KNOWS_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).vertices(__.hasLabel("person")).edges(__.<Vertex>bothE("knows").has("weight", P.gt(0.5f))).mapReduce(new MapReduceJ(VertexProgramM.PEOPLE_KNOWS_WELL_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).edges(__.<Vertex>bothE().limit(0)).mapReduce(new MapReduceJ(VertexProgramM.VERTICES_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).edges(__.<Vertex>outE().limit(1)).mapReduce(new MapReduceJ(VertexProgramM.ONE_OUT_EDGE_ONLY)).submit().get();
        graphProvider.getGraphComputer(graph).edges(outE()).mapReduce(new MapReduceJ(VertexProgramM.OUT_EDGES_ONLY)).submit().get();

        // EXCEPTION HANDLING
        try {
            graphProvider.getGraphComputer(graph).vertices(__.out());
            fail();
        } catch (final IllegalArgumentException e) {
            assertEquals(e.getMessage(), GraphComputer.Exceptions.vertexFilterAccessesIncidentEdges(__.out()).getMessage());
        }
        try {
            graphProvider.getGraphComputer(graph).edges(__.<Vertex>out().outE());
            fail();
        } catch (final IllegalArgumentException e) {
            assertEquals(e.getMessage(), GraphComputer.Exceptions.edgeFilterAccessesAdjacentVertices(__.<Vertex>out().outE()).getMessage());
        }
    }

    public static class VertexProgramM implements VertexProgram {

        public static final String SOFTWARE_ONLY = "softwareOnly";
        public static final String PEOPLE_ONLY = "peopleOnly";
        public static final String KNOWS_ONLY = "knowsOnly";
        public static final String PEOPLE_KNOWS_ONLY = "peopleKnowsOnly";
        public static final String PEOPLE_KNOWS_WELL_ONLY = "peopleKnowsWellOnly";
        public static final String VERTICES_ONLY = "verticesOnly";
        public static final String ONE_OUT_EDGE_ONLY = "oneOutEdgeOnly";
        public static final String OUT_EDGES_ONLY = "outEdgesOnly";

        private String state;

        public VertexProgramM() {

        }

        public VertexProgramM(final String state) {
            this.state = state;
        }

        @Override
        public void setup(final Memory memory) {

        }

        @Override
        public void execute(final Vertex vertex, final Messenger messenger, final Memory memory) {
            switch (this.state) {
                case SOFTWARE_ONLY: {
                    assertEquals("software", vertex.label());
                    assertFalse(vertex.edges(Direction.OUT).hasNext());
                    assertTrue(vertex.edges(Direction.IN).hasNext());
                    assertTrue(vertex.edges(Direction.IN, "created").hasNext());
                    assertFalse(vertex.edges(Direction.IN, "knows").hasNext());
                    break;
                }
                case PEOPLE_ONLY: {
                    assertEquals("person", vertex.label());
                    assertFalse(vertex.edges(Direction.IN, "created").hasNext());
                    assertTrue(IteratorUtils.count(vertex.edges(Direction.BOTH)) > 0);
                    break;
                }
                case KNOWS_ONLY: {
                    assertEquals(0, IteratorUtils.count(vertex.edges(Direction.BOTH, "created")));
                    if (vertex.value("name").equals("marko"))
                        assertEquals(2, IteratorUtils.count(vertex.edges(Direction.BOTH, "knows")));
                    else if (vertex.value("name").equals("vadas"))
                        assertEquals(1, IteratorUtils.count(vertex.edges(Direction.IN, "knows")));
                    else if (vertex.value("name").equals("josh"))
                        assertEquals(1, IteratorUtils.count(vertex.edges(Direction.IN, "knows")));
                    else {
                        assertEquals(0, IteratorUtils.count(vertex.edges(Direction.BOTH, "knows")));
                        assertEquals(0, IteratorUtils.count(vertex.edges(Direction.BOTH)));
                    }
                    break;
                }
                case PEOPLE_KNOWS_ONLY: {
                    assertEquals("person", vertex.label());
                    assertEquals(0, IteratorUtils.count(vertex.edges(Direction.BOTH, "created")));
                    if (vertex.value("name").equals("marko"))
                        assertEquals(2, IteratorUtils.count(vertex.edges(Direction.BOTH, "knows")));
                    else if (vertex.value("name").equals("vadas"))
                        assertEquals(1, IteratorUtils.count(vertex.edges(Direction.IN, "knows")));
                    else if (vertex.value("name").equals("josh"))
                        assertEquals(1, IteratorUtils.count(vertex.edges(Direction.IN, "knows")));
                    else {
                        assertEquals(0, IteratorUtils.count(vertex.edges(Direction.BOTH, "knows")));
                        assertEquals(0, IteratorUtils.count(vertex.edges(Direction.BOTH)));
                    }
                    break;
                }
                case PEOPLE_KNOWS_WELL_ONLY: {
                    assertEquals("person", vertex.label());
                    assertEquals(0, IteratorUtils.count(vertex.edges(Direction.BOTH, "created")));
                    if (vertex.value("name").equals("marko")) {
                        assertEquals(1, IteratorUtils.count(vertex.edges(Direction.BOTH, "knows")));
                        assertEquals(1.0, vertex.edges(Direction.OUT, "knows").next().value("weight"), 0.001);
                    } else if (vertex.value("name").equals("vadas"))
                        assertEquals(0, IteratorUtils.count(vertex.edges(Direction.IN, "knows")));
                    else if (vertex.value("name").equals("josh")) {
                        assertEquals(1, IteratorUtils.count(vertex.edges(Direction.IN, "knows")));
                        assertEquals(1.0, vertex.edges(Direction.IN, "knows").next().value("weight"), 0.001);
                    } else {
                        assertEquals(0, IteratorUtils.count(vertex.edges(Direction.BOTH, "knows")));
                        assertEquals(0, IteratorUtils.count(vertex.edges(Direction.BOTH)));
                    }
                    break;
                }
                case VERTICES_ONLY: {
                    assertEquals(0, IteratorUtils.count(vertex.edges(Direction.BOTH)));
                    break;
                }
                case ONE_OUT_EDGE_ONLY: {
                    if (vertex.label().equals("software") || vertex.value("name").equals("vadas"))
                        assertEquals(0, IteratorUtils.count(vertex.edges(Direction.BOTH)));
                    else {
                        assertEquals(1, IteratorUtils.count(vertex.edges(Direction.OUT)));
                        assertEquals(0, IteratorUtils.count(vertex.edges(Direction.IN)));
                        assertEquals(1, IteratorUtils.count(vertex.edges(Direction.BOTH)));
                    }
                    break;
                }
                case OUT_EDGES_ONLY: {
                    if (vertex.label().equals("software") || vertex.value("name").equals("vadas"))
                        assertEquals(0, IteratorUtils.count(vertex.edges(Direction.BOTH)));
                    else {
                        assertTrue(IteratorUtils.count(vertex.edges(Direction.OUT)) > 0);
                        assertEquals(0, IteratorUtils.count(vertex.edges(Direction.IN)));
                        assertEquals(IteratorUtils.count(vertex.edges(Direction.OUT)), IteratorUtils.count(vertex.edges(Direction.BOTH)));
                    }
                    break;
                }
                default:
                    throw new IllegalStateException("This is an illegal state for this test case: " + this.state);
            }
        }

        @Override
        public boolean terminate(final Memory memory) {
            return true;
        }

        @Override
        public Set<MessageScope> getMessageScopes(Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.NOTHING;
        }

        @Override
        @SuppressWarnings("CloneDoesntCallSuperClone,CloneDoesntDeclareCloneNotSupportedException")
        public VertexProgramM clone() {
            return new VertexProgramM(this.state);
        }

        @Override
        public void loadState(final Graph graph, final Configuration configuration) {
            this.state = configuration.getString("state");
        }

        @Override
        public void storeState(final Configuration configuration) {
            configuration.setProperty("state", this.state);
            VertexProgram.super.storeState(configuration);
        }

    }

    private static class MapReduceJ implements MapReduce<MapReduce.NullObject, Integer, MapReduce.NullObject, Integer, Integer> {

        private String state;

        public MapReduceJ() {
        }

        public MapReduceJ(final String state) {
            this.state = state;
        }

        @Override
        public void loadState(final Graph graph, final Configuration configuration) {
            this.state = configuration.getString("state");
        }

        @Override
        public void storeState(final Configuration configuration) {
            configuration.setProperty("state", this.state);
            MapReduce.super.storeState(configuration);
        }

        @Override
        @SuppressWarnings("CloneDoesntCallSuperClone,CloneDoesntDeclareCloneNotSupportedException")
        public MapReduceJ clone() {
            return new MapReduceJ(this.state);
        }

        @Override
        public boolean doStage(final Stage stage) {
            return true;
        }

        @Override
        public void map(final Vertex vertex, final MapEmitter<NullObject, Integer> emitter) {
            emitter.emit(1);
            switch (this.state) {
                case VertexProgramM.SOFTWARE_ONLY: {
                    assertEquals("software", vertex.label());
                    break;
                }
                case VertexProgramM.PEOPLE_ONLY: {
                    assertEquals("person", vertex.label());
                    break;
                }
                case VertexProgramM.KNOWS_ONLY: {
                    assertTrue(vertex.label().equals("person") || vertex.label().equals("software"));
                    break;
                }
                case VertexProgramM.PEOPLE_KNOWS_ONLY: {
                    assertEquals("person", vertex.label());
                    break;
                }
                case VertexProgramM.PEOPLE_KNOWS_WELL_ONLY: {
                    assertEquals("person", vertex.label());
                    break;
                }
                case VertexProgramM.VERTICES_ONLY: {
                    assertTrue(vertex.label().equals("person") || vertex.label().equals("software"));
                    break;
                }
                case VertexProgramM.ONE_OUT_EDGE_ONLY: {
                    assertTrue(vertex.label().equals("person") || vertex.label().equals("software"));
                    break;
                }
                case VertexProgramM.OUT_EDGES_ONLY: {
                    assertTrue(vertex.label().equals("person") || vertex.label().equals("software"));
                    break;
                }
                default:
                    throw new IllegalStateException("This is an illegal state for this test case: " + this.state);
            }
        }

        @Override
        public void combine(final NullObject key, final Iterator<Integer> values, final ReduceEmitter<NullObject, Integer> emitter) {
            this.reduce(key, values, emitter);
        }

        @Override
        public void reduce(final NullObject key, final Iterator<Integer> values, final ReduceEmitter<NullObject, Integer> emitter) {
            int count = 0;
            while (values.hasNext()) {
                count = count + values.next();
            }
            emitter.emit(count);
        }

        @Override
        public Integer generateFinalResult(final Iterator<KeyValue<NullObject, Integer>> keyValues) {
            int counter = keyValues.next().getValue();
            assertFalse(keyValues.hasNext());

            switch (this.state) {
                case VertexProgramM.SOFTWARE_ONLY: {
                    assertEquals(2, counter);
                    break;
                }
                case VertexProgramM.PEOPLE_ONLY: {
                    assertEquals(4, counter);
                    break;
                }
                case VertexProgramM.KNOWS_ONLY: {
                    assertEquals(6, counter);
                    break;
                }
                case VertexProgramM.PEOPLE_KNOWS_ONLY: {
                    assertEquals(4, counter);
                    break;
                }
                case VertexProgramM.PEOPLE_KNOWS_WELL_ONLY: {
                    assertEquals(4, counter);
                    break;
                }
                case VertexProgramM.VERTICES_ONLY: {
                    assertEquals(6, counter);
                    break;
                }
                case VertexProgramM.ONE_OUT_EDGE_ONLY: {
                    assertEquals(6, counter);
                    break;
                }
                case VertexProgramM.OUT_EDGES_ONLY: {
                    assertEquals(6, counter);
                    break;
                }
                default:
                    throw new IllegalStateException("This is an illegal state for this test case: " + this.state);
            }
            return counter;
        }

        @Override
        public String getMemoryKey() {
            return "a";
        }
    }

    @Test
    @LoadGraphWith(MODERN)
    public void shouldSupportJobChaining() throws Exception {
        final ComputerResult result1 = graphProvider.getGraphComputer(graph)
                .program(PageRankVertexProgram.build().iterations(5).create(graph)).persist(GraphComputer.Persist.EDGES).result(GraphComputer.ResultGraph.NEW).submit().get();
        final Graph graph1 = result1.graph();
        final Memory memory1 = result1.memory();
        assertEquals(5, memory1.getIteration());
        assertEquals(6, graph1.traversal().V().count().next().intValue());
        assertEquals(6, graph1.traversal().E().count().next().intValue());
        assertEquals(6, graph1.traversal().V().values(PageRankVertexProgram.PAGE_RANK).count().next().intValue());
        assertEquals(18, graph1.traversal().V().values().count().next().intValue());
        //
        final ComputerResult result2 = graph1.compute(graphProvider.getGraphComputer(graph1).getClass())
                .program(PeerPressureVertexProgram.build().maxIterations(4).create(graph1)).persist(GraphComputer.Persist.EDGES).result(GraphComputer.ResultGraph.NEW).submit().get();
        final Graph graph2 = result2.graph();
        final Memory memory2 = result2.memory();
        assertTrue(memory2.getIteration() <= 4);
        assertEquals(6, graph2.traversal().V().count().next().intValue());
        assertEquals(6, graph2.traversal().E().count().next().intValue());
        assertEquals(6, graph2.traversal().V().values(PeerPressureVertexProgram.CLUSTER).count().next().intValue());
        assertEquals(6, graph2.traversal().V().values(PageRankVertexProgram.PAGE_RANK).count().next().intValue());
        assertEquals(24, graph2.traversal().V().values().count().next().intValue());
        //
        final ComputerResult result3 = graph2.compute(graphProvider.getGraphComputer(graph2).getClass()).
                program(TraversalVertexProgram.build().traversal(
                        g.V().groupCount("m").
                                by(__.values(PageRankVertexProgram.PAGE_RANK).count()).
                              label().asAdmin()).create(graph2)).
                persist(GraphComputer.Persist.EDGES).result(GraphComputer.ResultGraph.NEW).submit().get();
        final Graph graph3 = result3.graph();
        final Memory memory3 = result3.memory();
        assertTrue(memory3.keys().contains("m"));
        assertTrue(memory3.keys().contains(TraversalVertexProgram.HALTED_TRAVERSERS));
        assertEquals(1, memory3.<Map<Long, Long>>get("m").size());
        assertEquals(6, memory3.<Map<Long, Long>>get("m").get(1l).intValue());
        List<Traverser<String>> traversers = IteratorUtils.list(memory3.<TraverserSet>get(TraversalVertexProgram.HALTED_TRAVERSERS).iterator());
        assertEquals(6l, traversers.stream().map(Traverser::bulk).reduce((a, b) -> a + b).get().longValue());
        assertEquals(4l, traversers.stream().filter(s -> s.get().equals("person")).map(Traverser::bulk).reduce((a, b) -> a + b).get().longValue());
        assertEquals(2l, traversers.stream().filter(s -> s.get().equals("software")).map(Traverser::bulk).reduce((a, b) -> a + b).get().longValue());
        assertEquals(6, graph3.traversal().V().count().next().intValue());
        assertEquals(6, graph3.traversal().E().count().next().intValue());
        assertEquals(0, graph3.traversal().V().values(TraversalVertexProgram.HALTED_TRAVERSERS).count().next().intValue());
        assertEquals(6, graph3.traversal().V().values(PeerPressureVertexProgram.CLUSTER).count().next().intValue());
        assertEquals(6, graph3.traversal().V().values(PageRankVertexProgram.PAGE_RANK).count().next().intValue());
        assertEquals(24, graph3.traversal().V().values().count().next().intValue()); // no halted traversers

        // TODO: add a test the shows DAG behavior -- splitting another TraversalVertexProgram off of the PeerPressureVertexProgram job.
    }

    ///////////////////////////////////

    @Test
    @LoadGraphWith(MODERN)
    public void shouldSupportPreExistingComputeKeys() throws Exception {
        final ComputerResult result = graphProvider.getGraphComputer(graph).program(new VertexProgramN()).submit().get();
        result.graph().vertices().forEachRemaining(vertex -> {
            if (vertex.label().equals("person")) {
                if (vertex.value("name").equals("marko"))
                    assertEquals(32, vertex.<Integer>value("age").intValue());
                else if (vertex.value("name").equals("peter"))
                    assertEquals(38, vertex.<Integer>value("age").intValue());
                else if (vertex.value("name").equals("vadas"))
                    assertEquals(30, vertex.<Integer>value("age").intValue());
                else if (vertex.value("name").equals("josh"))
                    assertEquals(35, vertex.<Integer>value("age").intValue());
                else
                    throw new IllegalStateException("This vertex should not have been accessed: " + vertex);
            }
        });
    }

    private static class VertexProgramN extends StaticVertexProgram {

        @Override
        public void setup(final Memory memory) {

        }

        @Override
        public void execute(final Vertex vertex, final Messenger messenger, final Memory memory) {
            if (vertex.label().equals("person"))
                vertex.property(VertexProperty.Cardinality.single, "age", vertex.<Integer>value("age") + 1);
        }

        @Override
        public boolean terminate(final Memory memory) {
            return memory.getIteration() > 1;
        }

        @Override
        public Set<MessageScope> getMessageScopes(final Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public Set<VertexComputeKey> getVertexComputeKeys() {
            return Collections.singleton(VertexComputeKey.of("age", false));
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.VERTEX_PROPERTIES;
        }
    }

    ///////////////////////////////////

    @Test
    @LoadGraphWith(MODERN)
    public void shouldSupportTransientKeys() throws Exception {
        final ComputerResult result = graphProvider.getGraphComputer(graph).program(new VertexProgramO()).mapReduce(new MapReduceK()).submit().get();
        result.graph().vertices().forEachRemaining(vertex -> {
            assertFalse(vertex.property("v1").isPresent());
            assertFalse(vertex.property("v2").isPresent());
            assertTrue(vertex.property("v3").isPresent());
            assertEquals("shouldExist", vertex.value("v3"));
            assertTrue(vertex.property("name").isPresent());
            if (vertex.label().equals("software"))
                assertTrue(vertex.property("lang").isPresent());
            else
                assertTrue(vertex.property("age").isPresent());
            assertEquals(3, IteratorUtils.count(vertex.properties()));
            assertEquals(0, IteratorUtils.count(vertex.properties("v1")));
            assertEquals(0, IteratorUtils.count(vertex.properties("v2")));
            assertEquals(1, IteratorUtils.count(vertex.properties("v3")));
            assertEquals(1, IteratorUtils.count(vertex.properties("name")));
        });
        assertEquals(6l, result.graph().traversal().V().properties("name").count().next().longValue());
        assertEquals(0l, result.graph().traversal().V().properties("v1").count().next().longValue());
        assertEquals(0l, result.graph().traversal().V().properties("v2").count().next().longValue());
        assertEquals(6l, result.graph().traversal().V().properties("v3").count().next().longValue());
        assertEquals(6l, result.graph().traversal().V().<String>values("name").dedup().count().next().longValue());
        assertEquals(1l, result.graph().traversal().V().<String>values("v3").dedup().count().next().longValue());

        final Traversal<Vertex,String> t = result.graph().traversal().V().<String>values("v3").dedup();
        assertEquals("shouldExist", t.next());
        CloseableIterator.closeIterator(t);

        ///
        assertFalse(result.memory().exists("m1"));
        assertFalse(result.memory().exists("m2"));
        assertTrue(result.memory().exists("m3"));
        assertEquals(24l, result.memory().<Long>get("m3").longValue());
        assertEquals(2, result.memory().keys().size());  // mapReduceK
    }

    private static class VertexProgramO extends StaticVertexProgram {

        @Override
        public void setup(final Memory memory) {
            assertFalse(memory.exists("m1"));
            assertFalse(memory.exists("m2"));
            assertFalse(memory.exists("m3"));
        }

        @Override
        public void execute(final Vertex vertex, final Messenger messenger, final Memory memory) {
            if (memory.isInitialIteration()) {
                assertFalse(vertex.property("v1").isPresent());
                assertFalse(vertex.property("v2").isPresent());
                assertFalse(vertex.property("v3").isPresent());
                vertex.property("v1", "shouldNotExist");
                vertex.property("v2", "shouldNotExist");
                vertex.property("v3", "shouldExist");
                assertTrue(vertex.property("v1").isPresent());
                assertTrue(vertex.property("v2").isPresent());
                assertTrue(vertex.property("v3").isPresent());
                assertEquals("shouldNotExist", vertex.value("v1"));
                assertEquals("shouldNotExist", vertex.value("v2"));
                assertEquals("shouldExist", vertex.value("v3"));
                //
                assertFalse(memory.exists("m1"));
                assertFalse(memory.exists("m2"));
                assertFalse(memory.exists("m3"));
                memory.add("m1", false);
                memory.add("m2", true);
                memory.add("m3", 2l);
                // should still not exist as this pulls from the master memory
                assertFalse(memory.exists("m1"));
                assertFalse(memory.exists("m2"));
                assertFalse(memory.exists("m3"));

            } else {
                assertTrue(vertex.property("v1").isPresent());
                assertTrue(vertex.property("v2").isPresent());
                assertTrue(vertex.property("v3").isPresent());
                assertEquals("shouldNotExist", vertex.value("v1"));
                assertEquals("shouldNotExist", vertex.value("v2"));
                assertEquals("shouldExist", vertex.value("v3"));
                //
                assertTrue(memory.exists("m1"));
                assertTrue(memory.exists("m2"));
                assertTrue(memory.exists("m3"));
                assertFalse(memory.get("m1"));
                assertTrue(memory.get("m2"));
                assertEquals(12l, memory.<Long>get("m3").longValue());
                memory.add("m1", true);
                memory.add("m2", true);
                memory.add("m3", 2l);
            }
        }

        @Override
        public boolean terminate(final Memory memory) {
            assertTrue(memory.exists("m1"));
            assertTrue(memory.exists("m2"));
            assertTrue(memory.exists("m3"));
            if (memory.isInitialIteration()) {
                assertFalse(memory.get("m1"));
                assertTrue(memory.get("m2"));
                assertEquals(12l, memory.<Long>get("m3").longValue());
                return false;
            } else {
                assertTrue(memory.get("m1"));
                assertTrue(memory.get("m2"));
                assertEquals(24l, memory.<Long>get("m3").longValue());
                return true;
            }
        }

        @Override
        public Set<MessageScope> getMessageScopes(final Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public Set<MemoryComputeKey> getMemoryComputeKeys() {
            return new HashSet<>(Arrays.asList(
                    MemoryComputeKey.of("m1", Operator.or, true, true),
                    MemoryComputeKey.of("m2", Operator.and, true, true),
                    MemoryComputeKey.of("m3", Operator.sum, true, false)));
        }

        @Override
        public Set<VertexComputeKey> getVertexComputeKeys() {
            return new HashSet<>(Arrays.asList(
                    VertexComputeKey.of("v1", true),
                    VertexComputeKey.of("v2", true),
                    VertexComputeKey.of("v3", false)));
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.VERTEX_PROPERTIES;
        }
    }

    public static class MapReduceK extends StaticMapReduce {

        @Override
        public boolean doStage(final Stage stage) {
            return stage.equals(Stage.MAP);
        }

        @Override
        public void map(final Vertex vertex, final MapEmitter emitter) {
            assertFalse(vertex.property("v1").isPresent());
            assertFalse(vertex.property("v2").isPresent());
            assertTrue(vertex.property("v3").isPresent());
            assertTrue(vertex.property("name").isPresent());
            assertEquals(3, IteratorUtils.count(vertex.properties()));
            assertEquals(3, IteratorUtils.count(vertex.values()));
        }

        @Override
        public String getMemoryKey() {
            return "mapReduceK";
        }

        @Override
        public Object generateFinalResult(final Iterator keyValues) {
            return "anObject";
        }
    }

    ///////////////////////////////////

    @Test
    @LoadGraphWith(MODERN)
    public void shouldSupportBroadcastKeys() throws Exception {
        final ComputerResult result = graphProvider.getGraphComputer(graph).program(new VertexProgramP()).submit().get();
        assertTrue(result.memory().exists("m1"));
        assertFalse(result.memory().exists("m2"));
        assertFalse(result.memory().exists("m3"));
        assertTrue(result.memory().exists("m4"));
        assertTrue(result.memory().get("m1"));
        assertEquals(-18, result.memory().<Integer>get("m4").intValue());
        assertEquals(2, result.memory().keys().size());
    }

    private static class VertexProgramP extends StaticVertexProgram {

        @Override
        public void setup(final Memory memory) {
            assertFalse(memory.exists("m1"));  // or
            assertFalse(memory.exists("m2"));  // and
            assertFalse(memory.exists("m3"));  // long
            assertFalse(memory.exists("m4"));  // int
            memory.set("m1", false);
            memory.set("m2", true);
            memory.set("m3", 0l);
            memory.set("m4", 0);
        }

        @Override
        public void execute(final Vertex vertex, final Messenger messenger, final Memory memory) {
            if (memory.isInitialIteration()) {
                assertFalse(memory.exists("m1"));
                assertTrue(memory.exists("m2"));
                assertTrue(memory.get("m2"));
                assertFalse(memory.exists("m3"));
                assertTrue(memory.exists("m4"));
                assertEquals(0, memory.<Integer>get("m4").intValue());
                memory.add("m1", false);
                memory.add("m2", true);
                memory.add("m3", 1l);
                memory.add("m4", -1);
            } else {
                assertFalse(memory.exists("m1")); // no broadcast
                assertTrue(memory.exists("m2"));
                assertFalse(memory.exists("m3")); // no broadcast
                assertTrue(memory.exists("m4"));
                try {
                    assertFalse(memory.get("m1"));
                    fail();
                } catch (final Exception e) {
                    validateException(Memory.Exceptions.memoryDoesNotExist("m1"), e);
                }
                assertTrue(memory.get("m2"));
                try {
                    assertEquals(6l, memory.<Long>get("m3").longValue());
                    fail();
                } catch (final Exception e) {
                    validateException(Memory.Exceptions.memoryDoesNotExist("m3"), e);
                }
                assertEquals(-6l, memory.<Integer>get("m4").intValue());
                ///
                memory.add("m1", true);
                memory.add("m2", true);
                memory.add("m3", 2l);
                memory.add("m4", -2);
            }
        }

        @Override
        public boolean terminate(final Memory memory) {
            assertTrue(memory.exists("m1"));
            assertTrue(memory.exists("m2"));
            assertTrue(memory.exists("m3"));
            assertTrue(memory.exists("m4"));
            if (memory.isInitialIteration()) {
                assertFalse(memory.get("m1"));
                assertTrue(memory.get("m2"));
                assertEquals(6l, memory.<Long>get("m3").longValue());
                assertEquals(-6, memory.<Integer>get("m4").intValue());
                return false;
            } else {
                assertTrue(memory.get("m1"));
                assertTrue(memory.get("m2"));
                assertEquals(18l, memory.<Long>get("m3").longValue());
                assertEquals(-18, memory.<Integer>get("m4").intValue());
                return true;
            }
        }

        @Override
        public Set<MessageScope> getMessageScopes(final Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public Set<MemoryComputeKey> getMemoryComputeKeys() {
            return new HashSet<>(Arrays.asList(
                    MemoryComputeKey.of("m1", Operator.or, false, false),
                    MemoryComputeKey.of("m2", Operator.and, true, true),
                    MemoryComputeKey.of("m3", Operator.sum, false, true),
                    MemoryComputeKey.of("m4", Operator.sum, true, false)));
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.VERTEX_PROPERTIES;
        }
    }

    ///////////////////////////////////

    
    @Test
    @LoadGraphWith(MODERN)
    public void shouldSucceedWithProperTraverserRequirements() throws Exception {
        final VertexProgramQ vp = VertexProgramQ.build().property("pl").create();
        final Map<String, List<Integer>> expected = new HashMap<>();
        expected.put("vadas", Collections.singletonList(2));
        expected.put("lop", Arrays.asList(2, 2, 2, 3));
        expected.put("josh", Collections.singletonList(2));
        expected.put("ripple", Arrays.asList(2, 3));

        try {
            g.V().repeat(__.out()).emit().program(vp).dedup()
                    .valueMap("name", "pl").forEachRemaining((Map<Object, Object> map) -> {

                final String name = (String) ((List) map.get("name")).get(0);
                final List<Integer> pathLengths = (List<Integer>) map.get("pl");
                assertTrue(expected.containsKey(name));
                final List<Integer> expectedPathLengths = expected.remove(name);
                assertTrue(expectedPathLengths.containsAll(pathLengths));
                assertTrue(pathLengths.containsAll(expectedPathLengths));
            });

            assertTrue(expected.isEmpty());
        } catch (VerificationException ex) {
            assumeNoException(ex);
        }
    }

    @Test
    @LoadGraphWith(MODERN)
    public void shouldFailWithImproperTraverserRequirements() throws Exception {
        final VertexProgramQ vp = VertexProgramQ.build().property("pl").useTraverserRequirements(false).create();
        try {
            g.V().repeat(__.out()).emit().program(vp).dedup()
                    .forEachRemaining((Vertex v) -> assertFalse(v.property("pl").isPresent()));
        } catch (VerificationException ex) {
            assumeNoException(ex);
        }
    }

    private static class VertexProgramQ implements VertexProgram<Object> {

        private static final String VERTEX_PROGRAM_Q_CFG_PREFIX = "gremlin.vertexProgramQ";
        private static final String PROPERTY_CFG_KEY = VERTEX_PROGRAM_Q_CFG_PREFIX + ".property";
        private static final String LENGTHS_KEY = VERTEX_PROGRAM_Q_CFG_PREFIX + ".lengths";
        private static final String USE_TRAVERSER_REQUIREMENTS_CFG_KEY = VERTEX_PROGRAM_Q_CFG_PREFIX + ".useTraverserRequirements";

        private final static Set<MemoryComputeKey> MEMORY_COMPUTE_KEYS = Collections.singleton(
                MemoryComputeKey.of(LENGTHS_KEY, Operator.addAll, true, true)
        );

        private final Set<VertexComputeKey> elementComputeKeys;
        private Configuration configuration;
        private String propertyKey;
        private Set<TraverserRequirement> traverserRequirements;

        private VertexProgramQ() {
            elementComputeKeys = new HashSet<>();
        }

        public static Builder build() {
            return new Builder();
        }

        static class Builder extends AbstractVertexProgramBuilder<Builder> {

            private Builder() {
                super(VertexProgramQ.class);
            }

            @SuppressWarnings("unchecked")
            @Override
            public VertexProgramQ create(final Graph graph) {
                if (graph != null) {
                    ConfigurationUtils.append(graph.configuration().subset(VERTEX_PROGRAM_Q_CFG_PREFIX), configuration);
                }
                return (VertexProgramQ) VertexProgram.createVertexProgram(graph, configuration);
            }

            public Builder property(final String name) {
                configuration.setProperty(PROPERTY_CFG_KEY, name);
                return this;
            }

            /**
             * This is only configurable for the purpose of testing. In a real-world VP this would be a bad pattern.
             */
            public Builder useTraverserRequirements(final boolean value) {
                configuration.setProperty(USE_TRAVERSER_REQUIREMENTS_CFG_KEY, value);
                return this;
            }
        }

        @Override
        public void storeState(final Configuration config) {
            VertexProgram.super.storeState(config);
            if (configuration != null) {
                ConfigurationUtils.copy(configuration, config);
            }
        }


        @Override
        public void loadState(final Graph graph, final Configuration config) {
            configuration = new BaseConfiguration();
            if (config != null) {
                ConfigurationUtils.copy(config, configuration);
            }
            propertyKey = configuration.getString(PROPERTY_CFG_KEY);
            traverserRequirements = configuration.getBoolean(USE_TRAVERSER_REQUIREMENTS_CFG_KEY, true)
                    ? Collections.singleton(TraverserRequirement.PATH) : Collections.emptySet();
            elementComputeKeys.add(VertexComputeKey.of(propertyKey, false));
        }

        @Override
        public void setup(final Memory memory) {
        }

        @Override
        public void execute(final Vertex vertex, final Messenger messenger, final Memory memory) {
            if (memory.isInitialIteration()) {
                final Property<TraverserSet> haltedTraversers = vertex.property(TraversalVertexProgram.HALTED_TRAVERSERS);
                if (!haltedTraversers.isPresent()) return;
                final Iterator iterator = haltedTraversers.value().iterator();
                if (iterator.hasNext()) {
                    while (iterator.hasNext()) {
                        final Traverser t = (Traverser) iterator.next();
                        if (!(t.path() instanceof EmptyPath)) {
                            final int pathLength = t.path().size();
                            final List<Pair<Vertex, Integer>> memoryValue = new LinkedList<>();
                            memoryValue.add(Pair.with(vertex, pathLength));
                            memory.add(LENGTHS_KEY, memoryValue);
                        }
                    }
                }
            } else {
                if (memory.exists(LENGTHS_KEY)) {
                    final List<Pair<Vertex, Integer>> lengths = memory.get(LENGTHS_KEY);
                    for (final Pair<Vertex, Integer> pair : lengths) {
                        if (pair.getValue0().equals(vertex)) {
                            vertex.property(VertexProperty.Cardinality.list, propertyKey, pair.getValue1());
                        }
                    }
                }
            }
        }

        @Override
        public boolean terminate(final Memory memory) {
            return !memory.isInitialIteration();
        }

        @Override
        public Set<MessageScope> getMessageScopes(final Memory memory) {
            return Collections.emptySet();
        }

        @Override
        public Set<VertexComputeKey> getVertexComputeKeys() {
            return elementComputeKeys;
        }

        @Override
        public Set<MemoryComputeKey> getMemoryComputeKeys() {
            return MEMORY_COMPUTE_KEYS;
        }

        @SuppressWarnings({"CloneDoesntDeclareCloneNotSupportedException", "CloneDoesntCallSuperClone"})
        @Override
        public VertexProgram<Object> clone() {
            return this;
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.VERTEX_PROPERTIES;
        }

        @Override
        public Set<TraverserRequirement> getTraverserRequirements() {
            return this.traverserRequirements;
        }

        @Override
        public Features getFeatures() {
            return new Features() {
                @Override
                public boolean requiresVertexPropertyAddition() {
                    return true;
                }
            };
        }
    }

    ///////////////////////////////////

    @Test
    @LoadGraphWith(SINK)
    public void testMessagePassingIn() throws Exception {
        runMPTest(Direction.IN).forEachRemaining(v -> {
            vertexPropertyChecks(v);
            final String in = v.value(VertexProgramR.PROPERTY_IN);
            if (in.equals("a"))
                assertEquals("ab", v.value(VertexProgramR.PROPERTY_OUT).toString());
            else if (in.equals("b"))
                assertEquals("", v.value(VertexProgramR.PROPERTY_OUT).toString());
            else
                throw new IllegalStateException("This vertex should not exist: " + VertexProgramR.PROPERTY_IN
                        + "=" + String.valueOf(in));
        });
    }

    @Test
    @LoadGraphWith(SINK)
    public void testMessagePassingOut() throws Exception {
        runMPTest(Direction.OUT).forEachRemaining(v -> {
            vertexPropertyChecks(v);
            final String in = v.value(VertexProgramR.PROPERTY_IN);
            if (in.equals("a"))
                assertEquals("a", v.value(VertexProgramR.PROPERTY_OUT).toString());
            else if (in.equals("b"))
                assertEquals("a", v.value(VertexProgramR.PROPERTY_OUT).toString());
            else
                throw new IllegalStateException("This vertex should not exist: " + VertexProgramR.PROPERTY_IN
                        + "=" + String.valueOf(in));
        });
    }

    @Test
    @LoadGraphWith(SINK)
    public void testMessagePassingBoth() throws Exception {
        runMPTest(Direction.BOTH).forEachRemaining(v -> {
            vertexPropertyChecks(v);
            final String in = v.value(VertexProgramR.PROPERTY_IN);
            switch (in) {
                case "a":
                    assertEquals("aab", v.value(VertexProgramR.PROPERTY_OUT).toString());
                    break;
                case "b":
                    assertEquals("a", v.value(VertexProgramR.PROPERTY_OUT).toString());
                    break;
                default:
                    throw new IllegalStateException("This vertex should not exist: " + VertexProgramR.PROPERTY_IN
                            + "=" + String.valueOf(in));
            }
        });
    }

    private GraphTraversal<Vertex, Vertex> runMPTest(final Direction direction) throws Exception {
        final VertexProgramR svp = VertexProgramR.build().direction(direction).create();
        final ComputerResult result = graphProvider.getGraphComputer(graph).program(svp).vertices(__.hasLabel(VertexProgramR.VERTEX_LABEL)).submit().get();
        return result.graph().traversal().V().hasLabel(VertexProgramR.VERTEX_LABEL);
    }

    private static void vertexPropertyChecks(final Vertex v) {
        assertEquals(2, v.keys().size());
        assertTrue(v.keys().contains(VertexProgramR.PROPERTY_IN));
        assertTrue(v.keys().contains(VertexProgramR.PROPERTY_OUT));
        assertEquals(1, IteratorUtils.count(v.values(VertexProgramR.PROPERTY_IN)));
        assertEquals(1, IteratorUtils.count(v.values(VertexProgramR.PROPERTY_OUT)));
    }

    private static class VertexProgramR implements VertexProgram<String> {
        private static final String SIMPLE_VERTEX_PROGRAM_CFG_PREFIX = "gremlin.simpleVertexProgram";
        private static final String PROPERTY_OUT = "propertyout";
        private static final String PROPERTY_IN = "name";
        private static final String VERTEX_LABEL = "message";
        private static final String DIRECTION_CFG_KEY = SIMPLE_VERTEX_PROGRAM_CFG_PREFIX + ".direction";

        private Direction direction;
        private final MessageScope.Local<String> inMessageScope = MessageScope.Local.of(__::inE);
        private final MessageScope.Local<String> outMessageScope = MessageScope.Local.of(__::outE);
        private final MessageScope.Local<String> bothMessageScope = MessageScope.Local.of(__::bothE);
        private MessageScope.Local<String> messageScope;
        private final Set<VertexComputeKey> vertexComputeKeys = new HashSet<>(Arrays.asList(
                VertexComputeKey.of(VertexProgramR.PROPERTY_OUT, false),
                VertexComputeKey.of(VertexProgramR.PROPERTY_IN, false)));

        /**
         * Clones this vertex program.
         *
         * @return a clone of this vertex program
         */
        public VertexProgramR clone() { return this; }

        @Override
        public void loadState(final Graph graph, final Configuration configuration) {
            direction = Direction.valueOf(configuration.getString(DIRECTION_CFG_KEY));
            switch (direction) {
                case IN:
                    this.messageScope = this.inMessageScope;
                    break;
                case OUT:
                    this.messageScope = this.outMessageScope;
                    break;
                case BOTH:
                    this.messageScope = this.bothMessageScope;
                    break;
                default:
                    throw new IllegalStateException("Should not reach this point!");
            }
        }

        @Override
        public void storeState(final Configuration configuration) {
            VertexProgram.super.storeState(configuration);
            configuration.setProperty(DIRECTION_CFG_KEY, direction.name());
        }

        @Override
        public void setup(final Memory memory) {
        }

        @Override
        public void execute(final Vertex vertex, final Messenger<String> messenger, final Memory memory) {
            if (memory.isInitialIteration()) {
                messenger.sendMessage(this.messageScope, vertex.value(PROPERTY_IN).toString());
            } else {
                final char[] composite = IteratorUtils.reduce(messenger.receiveMessages(), "", (a, b) -> a + b).toCharArray();
                Arrays.sort(composite);
                vertex.property(PROPERTY_OUT, new String(composite));
            }
        }

        @Override
        public boolean terminate(final Memory memory) {
            return !memory.isInitialIteration();
        }

        @Override
        public Set<MessageScope> getMessageScopes(final Memory memory) {
            return Collections.singleton(this.messageScope);
        }

        @Override
        public GraphComputer.ResultGraph getPreferredResultGraph() {
            return GraphComputer.ResultGraph.NEW;
        }

        @Override
        public GraphComputer.Persist getPreferredPersist() {
            return GraphComputer.Persist.VERTEX_PROPERTIES;
        }

        @Override
        public Set<VertexComputeKey> getVertexComputeKeys() {
            return this.vertexComputeKeys;
        }

        @Override
        public Set<MemoryComputeKey> getMemoryComputeKeys() {
            return Collections.emptySet();
        }

        public static Builder build() {
            return new Builder();
        }

        static class Builder extends AbstractVertexProgramBuilder<Builder> {

            private Builder() {
                super(VertexProgramR.class);
            }

            @SuppressWarnings("unchecked")
            @Override
            public VertexProgramR create(final Graph graph) {
                if (graph != null) {
                    ConfigurationUtils.append(graph.configuration().subset(SIMPLE_VERTEX_PROGRAM_CFG_PREFIX), configuration);
                }
                return (VertexProgramR) VertexProgram.createVertexProgram(graph, configuration);
            }

            public Builder direction(final Direction direction) {
                configuration.setProperty(DIRECTION_CFG_KEY, direction.toString());
                return this;
            }
        }
    }
}
