diff --git a/java/src/main/java/org/softwareheritage/graph/rpc/GraphServer.java b/java/src/main/java/org/softwareheritage/graph/rpc/GraphServer.java index 3137b2b..9f06249 100644 --- a/java/src/main/java/org/softwareheritage/graph/rpc/GraphServer.java +++ b/java/src/main/java/org/softwareheritage/graph/rpc/GraphServer.java @@ -1,252 +1,269 @@ package org.softwareheritage.graph.rpc; import com.google.protobuf.FieldMask; import com.martiansoftware.jsap.*; import io.grpc.Server; import io.grpc.Status; import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; import io.grpc.netty.shaded.io.netty.channel.ChannelOption; import io.grpc.stub.StreamObserver; import io.grpc.protobuf.services.ProtoReflectionService; import it.unimi.dsi.logging.ProgressLogger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.softwareheritage.graph.SWHID; import org.softwareheritage.graph.SwhBidirectionalGraph; import org.softwareheritage.graph.compress.LabelMapBuilder; import java.io.FileInputStream; import java.io.IOException; import java.util.Properties; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * Server that manages startup/shutdown of a {@code Greeter} server. */ public class GraphServer { private final static Logger logger = LoggerFactory.getLogger(GraphServer.class); private final SwhBidirectionalGraph graph; private final int port; private final int threads; private Server server; public GraphServer(String graphBasename, int port, int threads) throws IOException { this.graph = loadGraph(graphBasename); this.port = port; this.threads = threads; } public static SwhBidirectionalGraph loadGraph(String basename) throws IOException { // TODO: use loadLabelledMapped() when https://github.com/vigna/webgraph-big/pull/5 is merged SwhBidirectionalGraph g = SwhBidirectionalGraph.loadLabelled(basename, new ProgressLogger(logger)); g.loadContentLength(); g.loadContentIsSkipped(); g.loadPersonIds(); g.loadAuthorTimestamps(); g.loadCommitterTimestamps(); g.loadMessages(); g.loadTagNames(); g.loadLabelNames(); return g; } private void start() throws IOException { server = NettyServerBuilder.forPort(port).withChildOption(ChannelOption.SO_REUSEADDR, true) .executor(Executors.newFixedThreadPool(threads)).addService(new TraversalService(graph)) .addService(ProtoReflectionService.newInstance()).build().start(); logger.info("Server started, listening on " + port); Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { GraphServer.this.stop(); } catch (InterruptedException e) { e.printStackTrace(System.err); } })); } private void stop() throws InterruptedException { if (server != null) { server.shutdown().awaitTermination(30, TimeUnit.SECONDS); } } /** * Await termination on the main thread since the grpc library uses daemon threads. */ private void blockUntilShutdown() throws InterruptedException { if (server != null) { server.awaitTermination(); } } private static JSAPResult parseArgs(String[] args) { JSAPResult config = null; try { SimpleJSAP jsap = new SimpleJSAP(LabelMapBuilder.class.getName(), "", new Parameter[]{ new FlaggedOption("port", JSAP.INTEGER_PARSER, "50091", JSAP.NOT_REQUIRED, 'p', "port", "The port on which the server should listen."), new FlaggedOption("threads", JSAP.INTEGER_PARSER, "1", JSAP.NOT_REQUIRED, 't', "threads", "The number of concurrent threads. 0 = number of cores."), new UnflaggedOption("graphBasename", JSAP.STRING_PARSER, JSAP.REQUIRED, "Basename of the output graph")}); config = jsap.parse(args); if (jsap.messagePrinted()) { System.exit(1); } } catch (JSAPException e) { e.printStackTrace(); } return config; } /** * Main launches the server from the command line. */ public static void main(String[] args) throws IOException, InterruptedException { JSAPResult config = parseArgs(args); String graphBasename = config.getString("graphBasename"); int port = config.getInt("port"); int threads = config.getInt("threads"); if (threads == 0) { threads = Runtime.getRuntime().availableProcessors(); } final GraphServer server = new GraphServer(graphBasename, port, threads); server.start(); server.blockUntilShutdown(); } static class TraversalService extends TraversalServiceGrpc.TraversalServiceImplBase { SwhBidirectionalGraph graph; public TraversalService(SwhBidirectionalGraph graph) { this.graph = graph; } @Override public void checkSwhid(CheckSwhidRequest request, StreamObserver responseObserver) { CheckSwhidResponse.Builder builder = CheckSwhidResponse.newBuilder().setExists(true); try { graph.getNodeId(new SWHID(request.getSwhid())); } catch (IllegalArgumentException e) { builder.setExists(false); builder.setDetails(e.getMessage()); } responseObserver.onNext(builder.build()); responseObserver.onCompleted(); } @Override public void stats(StatsRequest request, StreamObserver responseObserver) { StatsResponse.Builder response = StatsResponse.newBuilder(); response.setNumNodes(graph.numNodes()); response.setNumEdges(graph.numArcs()); Properties properties = new Properties(); try { properties.load(new FileInputStream(graph.getPath() + ".properties")); properties.load(new FileInputStream(graph.getPath() + ".stats")); } catch (IOException e) { throw new RuntimeException(e); } response.setCompression(Double.parseDouble(properties.getProperty("compratio"))); response.setBitsPerNode(Double.parseDouble(properties.getProperty("bitspernode"))); response.setBitsPerEdge(Double.parseDouble(properties.getProperty("bitsperlink"))); response.setAvgLocality(Double.parseDouble(properties.getProperty("avglocality"))); response.setIndegreeMin(Long.parseLong(properties.getProperty("minindegree"))); response.setIndegreeMax(Long.parseLong(properties.getProperty("maxindegree"))); response.setIndegreeAvg(Double.parseDouble(properties.getProperty("avgindegree"))); response.setOutdegreeMin(Long.parseLong(properties.getProperty("minoutdegree"))); response.setOutdegreeMax(Long.parseLong(properties.getProperty("maxoutdegree"))); response.setOutdegreeAvg(Double.parseDouble(properties.getProperty("avgoutdegree"))); responseObserver.onNext(response.build()); responseObserver.onCompleted(); } @Override public void getNode(GetNodeRequest request, StreamObserver responseObserver) { long nodeId; try { nodeId = graph.getNodeId(new SWHID(request.getSwhid())); - } catch (IllegalArgumentException e) { responseObserver.onError(Status.INVALID_ARGUMENT.withCause(e).asException()); return; } Node.Builder builder = Node.newBuilder(); NodePropertyBuilder.buildNodeProperties(graph.getForwardGraph(), request.hasMask() ? request.getMask() : null, builder, nodeId); responseObserver.onNext(builder.build()); responseObserver.onCompleted(); } @Override public void traverse(TraversalRequest request, StreamObserver responseObserver) { SwhBidirectionalGraph g = graph.copy(); - var t = new Traversal.SimpleTraversal(g, request, responseObserver::onNext); + Traversal.SimpleTraversal t; + try { + t = new Traversal.SimpleTraversal(g, request, responseObserver::onNext); + } catch (IllegalArgumentException e) { + responseObserver.onError(Status.INVALID_ARGUMENT.withCause(e).asException()); + return; + } t.visit(); responseObserver.onCompleted(); } @Override public void findPathTo(FindPathToRequest request, StreamObserver responseObserver) { SwhBidirectionalGraph g = graph.copy(); - var t = new Traversal.FindPathTo(g, request); + Traversal.FindPathTo t; + try { + t = new Traversal.FindPathTo(g, request); + } catch (IllegalArgumentException e) { + responseObserver.onError(Status.INVALID_ARGUMENT.withCause(e).asException()); + return; + } t.visit(); Path path = t.getPath(); if (path == null) { responseObserver.onError(Status.NOT_FOUND.asException()); } else { responseObserver.onNext(path); responseObserver.onCompleted(); } } @Override public void findPathBetween(FindPathBetweenRequest request, StreamObserver responseObserver) { SwhBidirectionalGraph g = graph.copy(); - var t = new Traversal.FindPathBetween(g, request); + Traversal.FindPathBetween t; + try { + t = new Traversal.FindPathBetween(g, request); + } catch (IllegalArgumentException e) { + responseObserver.onError(Status.INVALID_ARGUMENT.withCause(e).asException()); + return; + } t.visit(); Path path = t.getPath(); if (path == null) { responseObserver.onError(Status.NOT_FOUND.asException()); } else { responseObserver.onNext(path); responseObserver.onCompleted(); } } @Override public void countNodes(TraversalRequest request, StreamObserver responseObserver) { AtomicInteger count = new AtomicInteger(0); SwhBidirectionalGraph g = graph.copy(); TraversalRequest fixedReq = TraversalRequest.newBuilder(request) // Ignore return fields, just count nodes .setMask(FieldMask.getDefaultInstance()).build(); var t = new Traversal.SimpleTraversal(g, request, n -> count.incrementAndGet()); t.visit(); CountResponse response = CountResponse.newBuilder().setCount(count.get()).build(); responseObserver.onNext(response); responseObserver.onCompleted(); } @Override public void countEdges(TraversalRequest request, StreamObserver responseObserver) { AtomicInteger count = new AtomicInteger(0); SwhBidirectionalGraph g = graph.copy(); TraversalRequest fixedReq = TraversalRequest.newBuilder(request) // Force return empty successors to count the edges .setMask(FieldMask.newBuilder().addPaths("successor").build()).build(); var t = new Traversal.SimpleTraversal(g, request, n -> count.addAndGet(n.getSuccessorCount())); t.visit(); CountResponse response = CountResponse.newBuilder().setCount(count.get()).build(); responseObserver.onNext(response); responseObserver.onCompleted(); } } } diff --git a/java/src/test/java/org/softwareheritage/graph/rpc/FindPathBetweenTest.java b/java/src/test/java/org/softwareheritage/graph/rpc/FindPathBetweenTest.java index d450e9d..6297324 100644 --- a/java/src/test/java/org/softwareheritage/graph/rpc/FindPathBetweenTest.java +++ b/java/src/test/java/org/softwareheritage/graph/rpc/FindPathBetweenTest.java @@ -1,117 +1,146 @@ package org.softwareheritage.graph.rpc; import io.grpc.Status; import io.grpc.StatusRuntimeException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.softwareheritage.graph.SWHID; import java.util.ArrayList; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + public class FindPathBetweenTest extends TraversalServiceTest { private FindPathBetweenRequest.Builder getRequestBuilder(SWHID src, SWHID dst) { return FindPathBetweenRequest.newBuilder().addSrc(src.toString()).addDst(dst.toString()); } + @Test + public void testSwhidErrors() { + StatusRuntimeException thrown; + thrown = assertThrows(StatusRuntimeException.class, () -> client + .findPathBetween(FindPathBetweenRequest.newBuilder().addSrc(fakeSWHID("cnt", 404).toString()).build())); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + thrown = assertThrows(StatusRuntimeException.class, () -> client.findPathBetween(FindPathBetweenRequest + .newBuilder().addSrc("swh:1:lol:0000000000000000000000000000000000000001").build())); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + thrown = assertThrows(StatusRuntimeException.class, () -> client.findPathBetween(FindPathBetweenRequest + .newBuilder().addSrc("swh:1:cnt:000000000000000000000000000000000000000z").build())); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + thrown = assertThrows(StatusRuntimeException.class, + () -> client.findPathBetween(FindPathBetweenRequest.newBuilder().addSrc(TEST_ORIGIN_ID) + .addDst("swh:1:cnt:000000000000000000000000000000000000000z").build())); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + } + + @Test + public void testEdgeErrors() { + StatusRuntimeException thrown; + thrown = assertThrows(StatusRuntimeException.class, () -> client.findPathBetween(FindPathBetweenRequest + .newBuilder().addSrc(TEST_ORIGIN_ID).addDst(TEST_ORIGIN_ID).setEdges("batracien:reptile").build())); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + } + // Test path between ori 1 and cnt 4 (forward graph) @Test public void forwardRootToLeaf() { ArrayList actual = getSWHIDs( client.findPathBetween(getRequestBuilder(new SWHID(TEST_ORIGIN_ID), fakeSWHID("cnt", 4)).build())); List expected = List.of(new SWHID(TEST_ORIGIN_ID), fakeSWHID("snp", 20), fakeSWHID("rev", 9), fakeSWHID("dir", 8), fakeSWHID("dir", 6), fakeSWHID("cnt", 4)); Assertions.assertEquals(expected, actual); } // Test path between rev 18 and rev 3 (forward graph) @Test public void forwardRevToRev() { ArrayList actual = getSWHIDs( client.findPathBetween(getRequestBuilder(fakeSWHID("rev", 18), fakeSWHID("rev", 3)).build())); List expected = List.of(fakeSWHID("rev", 18), fakeSWHID("rev", 13), fakeSWHID("rev", 9), fakeSWHID("rev", 3)); Assertions.assertEquals(expected, actual); } // Test path between rev 3 and rev 18 (backward graph) @Test public void backwardRevToRev() { ArrayList actual = getSWHIDs( client.findPathBetween(getRequestBuilder(fakeSWHID("rev", 3), fakeSWHID("rev", 18)) .setDirection(GraphDirection.BACKWARD).build())); List expected = List.of(fakeSWHID("rev", 3), fakeSWHID("rev", 9), fakeSWHID("rev", 13), fakeSWHID("rev", 18)); Assertions.assertEquals(expected, actual); } // Test path between cnt 4 and itself (forward graph) @Test public void forwardCntToItself() { ArrayList actual = getSWHIDs( client.findPathBetween(getRequestBuilder(fakeSWHID("cnt", 4), fakeSWHID("cnt", 4)).build())); List expected = List.of(fakeSWHID("cnt", 4)); Assertions.assertEquals(expected, actual); } // Start from ori and rel 19 and find cnt 14 or cnt 7 (forward graph) @Test public void forwardMultipleSourcesDest() { ArrayList actual = getSWHIDs( client.findPathBetween(getRequestBuilder(fakeSWHID("rel", 19), fakeSWHID("cnt", 14)) .addSrc(TEST_ORIGIN_ID).addDst(fakeSWHID("cnt", 7).toString()).build())); List expected = List.of(fakeSWHID("rel", 19), fakeSWHID("rev", 18), fakeSWHID("dir", 17), fakeSWHID("cnt", 14)); } // Start from cnt 4 and cnt 11 and find rev 13 or rev 9 (backward graph) @Test public void backwardMultipleSourcesDest() { ArrayList actual = getSWHIDs(client.findPathBetween( getRequestBuilder(fakeSWHID("cnt", 4), fakeSWHID("rev", 13)).setDirection(GraphDirection.BACKWARD) .addSrc(fakeSWHID("cnt", 11).toString()).addDst(fakeSWHID("rev", 9).toString()).build())); List expected = List.of(fakeSWHID("cnt", 11), fakeSWHID("dir", 12), fakeSWHID("rev", 13)); Assertions.assertEquals(expected, actual); } // Start from all directories and find the origin (backward graph) @Test public void backwardMultipleSourcesAllDirToOri() { ArrayList actual = getSWHIDs( client.findPathBetween(getRequestBuilder(fakeSWHID("dir", 2), new SWHID(TEST_ORIGIN_ID)) .addSrc(fakeSWHID("dir", 6).toString()).addSrc(fakeSWHID("dir", 8).toString()) .addSrc(fakeSWHID("dir", 12).toString()).addSrc(fakeSWHID("dir", 16).toString()) .addSrc(fakeSWHID("dir", 17).toString()).setDirection(GraphDirection.BACKWARD).build())); List expected = List.of(fakeSWHID("dir", 8), fakeSWHID("rev", 9), fakeSWHID("snp", 20), new SWHID(TEST_ORIGIN_ID)); Assertions.assertEquals(expected, actual); } // Start from cnt 4 and find any rev (backward graph) @Test public void backwardCntToAnyRev() { ArrayList actual = getSWHIDs( client.findPathBetween(getRequestBuilder(fakeSWHID("cnt", 4), fakeSWHID("rev", 3)) .addDst(fakeSWHID("rev", 9).toString()).addDst(fakeSWHID("rev", 13).toString()) .addDst(fakeSWHID("rev", 18).toString()).setDirection(GraphDirection.BACKWARD).build())); List expected = List.of(fakeSWHID("cnt", 4), fakeSWHID("dir", 6), fakeSWHID("dir", 8), fakeSWHID("rev", 9)); Assertions.assertEquals(expected, actual); } // Impossible path between rev 9 and cnt 14 @Test public void forwardImpossiblePath() { StatusRuntimeException thrown = Assertions.assertThrows(StatusRuntimeException.class, () -> { client.findPathBetween(getRequestBuilder(fakeSWHID("rev", 9), fakeSWHID("cnt", 14)).build()); }); Assertions.assertEquals(thrown.getStatus(), Status.NOT_FOUND); // Reverse direction thrown = Assertions.assertThrows(StatusRuntimeException.class, () -> { client.findPathBetween(getRequestBuilder(fakeSWHID("cnt", 14), fakeSWHID("rev", 9)) .setDirection(GraphDirection.BACKWARD).build()); }); Assertions.assertEquals(thrown.getStatus(), Status.NOT_FOUND); } } diff --git a/java/src/test/java/org/softwareheritage/graph/rpc/FindPathToTest.java b/java/src/test/java/org/softwareheritage/graph/rpc/FindPathToTest.java index 2a583ec..6586a11 100644 --- a/java/src/test/java/org/softwareheritage/graph/rpc/FindPathToTest.java +++ b/java/src/test/java/org/softwareheritage/graph/rpc/FindPathToTest.java @@ -1,94 +1,128 @@ package org.softwareheritage.graph.rpc; import io.grpc.Status; import io.grpc.StatusRuntimeException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.softwareheritage.graph.SWHID; import java.util.ArrayList; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + public class FindPathToTest extends TraversalServiceTest { private FindPathToRequest.Builder getRequestBuilder(SWHID src, String allowedNodes) { return FindPathToRequest.newBuilder().addSrc(src.toString()) .setTarget(NodeFilter.newBuilder().setTypes(allowedNodes).build()); } + @Test + public void testSrcErrors() { + StatusRuntimeException thrown; + thrown = assertThrows(StatusRuntimeException.class, () -> client + .findPathTo(FindPathToRequest.newBuilder().addSrc(fakeSWHID("cnt", 404).toString()).build())); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + thrown = assertThrows(StatusRuntimeException.class, () -> client.findPathTo( + FindPathToRequest.newBuilder().addSrc("swh:1:lol:0000000000000000000000000000000000000001").build())); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + thrown = assertThrows(StatusRuntimeException.class, () -> client.findPathTo( + FindPathToRequest.newBuilder().addSrc("swh:1:cnt:000000000000000000000000000000000000000z").build())); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + } + + @Test + public void testEdgeErrors() { + StatusRuntimeException thrown; + thrown = assertThrows(StatusRuntimeException.class, () -> client.findPathTo( + FindPathToRequest.newBuilder().addSrc(TEST_ORIGIN_ID).setEdges("batracien:reptile").build())); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + } + + @Test + public void testTargetErrors() { + StatusRuntimeException thrown; + thrown = assertThrows(StatusRuntimeException.class, + () -> client.findPathTo(FindPathToRequest.newBuilder().addSrc(TEST_ORIGIN_ID) + .setTarget(NodeFilter.newBuilder().setTypes("argoumante,eglomatique").build()).build())); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + } + // Test path between ori 1 and any dir (forward graph) @Test public void forwardOriToFirstDir() { ArrayList actual = getSWHIDs( client.findPathTo(getRequestBuilder(new SWHID(TEST_ORIGIN_ID), "dir").build())); List expected = List.of(new SWHID(TEST_ORIGIN_ID), fakeSWHID("snp", 20), fakeSWHID("rev", 9), fakeSWHID("dir", 8)); Assertions.assertEquals(expected, actual); } // Test path between rel 19 and any cnt (forward graph) @Test public void forwardRelToFirstCnt() { ArrayList actual = getSWHIDs(client.findPathTo(getRequestBuilder(fakeSWHID("rel", 19), "cnt").build())); List expected = List.of(fakeSWHID("rel", 19), fakeSWHID("rev", 18), fakeSWHID("dir", 17), fakeSWHID("cnt", 14)); Assertions.assertEquals(expected, actual); } // Test path between dir 16 and any rel (backward graph) @Test public void backwardDirToFirstRel() { ArrayList actual = getSWHIDs(client.findPathTo( getRequestBuilder(fakeSWHID("dir", 16), "rel").setDirection(GraphDirection.BACKWARD).build())); List expected = List.of(fakeSWHID("dir", 16), fakeSWHID("dir", 17), fakeSWHID("rev", 18), fakeSWHID("rel", 19)); Assertions.assertEquals(expected, actual); } // Test path between cnt 4 and itself (forward graph) @Test public void forwardCntToItself() { ArrayList actual = getSWHIDs(client.findPathTo(getRequestBuilder(fakeSWHID("cnt", 4), "cnt").build())); List expected = List.of(fakeSWHID("cnt", 4)); Assertions.assertEquals(expected, actual); } // Start from ori and rel 19 and find any cnt (forward graph) @Test public void forwardMultipleSources() { ArrayList actual = getSWHIDs( client.findPathTo(getRequestBuilder(fakeSWHID("rel", 19), "cnt").addSrc(TEST_ORIGIN_ID).build())); List expected = List.of(fakeSWHID("rel", 19), fakeSWHID("rev", 18), fakeSWHID("dir", 17), fakeSWHID("cnt", 14)); } // Start from cnt 4 and cnt 11 and find any rev (backward graph) @Test public void backwardMultipleSources() { ArrayList actual = getSWHIDs(client.findPathTo(getRequestBuilder(fakeSWHID("cnt", 4), "rev") .addSrc(fakeSWHID("cnt", 11).toString()).setDirection(GraphDirection.BACKWARD).build())); List expected = List.of(fakeSWHID("cnt", 11), fakeSWHID("dir", 12), fakeSWHID("rev", 13)); Assertions.assertEquals(expected, actual); } // Start from all directories and find any origin (backward graph) @Test public void backwardMultipleSourcesAllDirToOri() { ArrayList actual = getSWHIDs(client.findPathTo(getRequestBuilder(fakeSWHID("dir", 2), "ori") .addSrc(fakeSWHID("dir", 6).toString()).addSrc(fakeSWHID("dir", 8).toString()) .addSrc(fakeSWHID("dir", 12).toString()).addSrc(fakeSWHID("dir", 16).toString()) .addSrc(fakeSWHID("dir", 17).toString()).setDirection(GraphDirection.BACKWARD).build())); List expected = List.of(fakeSWHID("dir", 8), fakeSWHID("rev", 9), fakeSWHID("snp", 20), new SWHID(TEST_ORIGIN_ID)); Assertions.assertEquals(expected, actual); } // Impossible path between rev 9 and any release (forward graph) @Test public void forwardImpossiblePath() { // Check that the return is STATUS.NOT_FOUND StatusRuntimeException thrown = Assertions.assertThrows(StatusRuntimeException.class, () -> { client.findPathTo(getRequestBuilder(fakeSWHID("rev", 9), "rel").build()); }); Assertions.assertEquals(thrown.getStatus(), Status.NOT_FOUND); } } diff --git a/java/src/test/java/org/softwareheritage/graph/rpc/GetNodeTest.java b/java/src/test/java/org/softwareheritage/graph/rpc/GetNodeTest.java index a57fee0..34783e7 100644 --- a/java/src/test/java/org/softwareheritage/graph/rpc/GetNodeTest.java +++ b/java/src/test/java/org/softwareheritage/graph/rpc/GetNodeTest.java @@ -1,279 +1,284 @@ package org.softwareheritage.graph.rpc; import com.google.protobuf.Descriptors; import com.google.protobuf.FieldMask; +import io.grpc.Status; import io.grpc.StatusRuntimeException; import org.junit.jupiter.api.Test; import org.softwareheritage.graph.SWHID; import java.util.*; import static org.junit.jupiter.api.Assertions.*; public class GetNodeTest extends TraversalServiceTest { @Test public void testNotFound() { - assertThrows(StatusRuntimeException.class, + StatusRuntimeException thrown = assertThrows(StatusRuntimeException.class, () -> client.getNode(GetNodeRequest.newBuilder().setSwhid(fakeSWHID("cnt", 404).toString()).build())); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); } @Test public void testInvalidSwhid() { - assertThrows(StatusRuntimeException.class, () -> client.getNode( + StatusRuntimeException thrown; + thrown = assertThrows(StatusRuntimeException.class, () -> client.getNode( GetNodeRequest.newBuilder().setSwhid("swh:1:lol:0000000000000000000000000000000000000001").build())); - assertThrows(StatusRuntimeException.class, () -> client.getNode( + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + thrown = assertThrows(StatusRuntimeException.class, () -> client.getNode( GetNodeRequest.newBuilder().setSwhid("swh:1:cnt:000000000000000000000000000000000000000z").build())); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); } @Test public void testContents() { List expectedCnts = List.of(1, 4, 5, 7, 11, 14, 15); Map expectedLengths = Map.of(1, 42, 4, 404, 5, 1337, 7, 666, 11, 313, 14, 14, 15, 404); Set expectedSkipped = Set.of(15); for (Integer cntId : expectedCnts) { Node n = client.getNode(GetNodeRequest.newBuilder().setSwhid(fakeSWHID("cnt", cntId).toString()).build()); assertTrue(n.hasCnt()); assertTrue(n.getCnt().hasLength()); assertEquals((long) expectedLengths.get(cntId), n.getCnt().getLength()); assertTrue(n.getCnt().hasIsSkipped()); assertEquals(expectedSkipped.contains(cntId), n.getCnt().getIsSkipped()); } } @Test public void testRevisions() { List expectedRevs = List.of(3, 9, 13, 18); Map expectedMessages = Map.of(3, "Initial commit", 9, "Add parser", 13, "Add tests", 18, "Refactor codebase"); Map expectedAuthors = Map.of(3, "foo", 9, "bar", 13, "foo", 18, "baz"); Map expectedCommitters = Map.of(3, "foo", 9, "bar", 13, "bar", 18, "foo"); Map expectedAuthorTimestamps = Map.of(3, 1111122220L, 9, 1111144440L, 13, 1111166660L, 18, 1111177770L); Map expectedCommitterTimestamps = Map.of(3, 1111122220L, 9, 1111155550L, 13, 1111166660L, 18, 1111177770L); Map expectedAuthorTimestampOffsets = Map.of(3, 120, 9, 120, 13, 120, 18, 0); Map expectedCommitterTimestampOffsets = Map.of(3, 120, 9, 120, 13, 120, 18, 0); HashMap personMapping = new HashMap<>(); for (Integer revId : expectedRevs) { Node n = client.getNode(GetNodeRequest.newBuilder().setSwhid(fakeSWHID("rev", revId).toString()).build()); assertTrue(n.hasRev()); assertTrue(n.getRev().hasMessage()); assertEquals(expectedMessages.get(revId), n.getRev().getMessage().toStringUtf8()); // Persons are anonymized, we just need to check that the mapping is self-consistent assertTrue(n.getRev().hasAuthor()); assertTrue(n.getRev().hasCommitter()); int[] actualPersons = new int[]{(int) n.getRev().getAuthor(), (int) n.getRev().getCommitter()}; String[] expectedPersons = new String[]{expectedAuthors.get(revId), expectedCommitters.get(revId)}; for (int i = 0; i < actualPersons.length; i++) { int actualPerson = actualPersons[i]; String expectedPerson = expectedPersons[i]; assertTrue(actualPerson >= 0); if (personMapping.containsKey(actualPerson)) { assertEquals(personMapping.get(actualPerson), expectedPerson); } else { personMapping.put(actualPerson, expectedPerson); } } assertTrue(n.getRev().hasAuthorDate()); assertTrue(n.getRev().hasAuthorDateOffset()); assertTrue(n.getRev().hasCommitterDate()); assertTrue(n.getRev().hasCommitterDateOffset()); // FIXME: all the timestamps are one hour off?! // System.err.println(revId + " " + n.getRev().getAuthorDate() + " " + // n.getRev().getAuthorDateOffset()); // System.err.println(revId + " " + n.getRev().getCommitterDate() + " " + // n.getRev().getCommitterDateOffset()); // assertEquals(expectedAuthorTimestamps.get(revId), n.getRev().getAuthorDate()); assertEquals(expectedAuthorTimestampOffsets.get(revId), n.getRev().getAuthorDateOffset()); // assertEquals(expectedCommitterTimestamps.get(revId), n.getRev().getAuthorDate()); assertEquals(expectedCommitterTimestampOffsets.get(revId), n.getRev().getAuthorDateOffset()); } } @Test public void testReleases() { List expectedRels = List.of(10, 19); Map expectedMessages = Map.of(10, "Version 1.0", 19, "Version 2.0"); Map expectedNames = Map.of(10, "v1.0", 19, "v2.0"); Map expectedAuthors = Map.of(10, "foo", 19, "bar"); Map expectedAuthorTimestamps = Map.of(10, 1234567890L); Map expectedAuthorTimestampOffsets = Map.of(3, 120); HashMap personMapping = new HashMap<>(); for (Integer relId : expectedRels) { Node n = client.getNode(GetNodeRequest.newBuilder().setSwhid(fakeSWHID("rel", relId).toString()).build()); assertTrue(n.hasRel()); assertTrue(n.getRel().hasMessage()); assertEquals(expectedMessages.get(relId), n.getRel().getMessage().toStringUtf8()); // FIXME: names are always empty?! // System.err.println(relId + " " + n.getRel().getName()); // assertEquals(expectedNames.get(relId), n.getRel().getName().toStringUtf8()); // Persons are anonymized, we just need to check that the mapping is self-consistent assertTrue(n.getRel().hasAuthor()); int actualPerson = (int) n.getRel().getAuthor(); String expectedPerson = expectedAuthors.get(relId); assertTrue(actualPerson >= 0); if (personMapping.containsKey(actualPerson)) { assertEquals(personMapping.get(actualPerson), expectedPerson); } else { personMapping.put(actualPerson, expectedPerson); } assertTrue(n.getRel().hasAuthorDate()); assertTrue(n.getRel().hasAuthorDateOffset()); // FIXME: all the timestamps are one hour off?! // if (expectedAuthorTimestamps.containsKey(relId)) { // assertEquals(expectedAuthorTimestamps.get(revId), n.getRev().getAuthorDate()); // } if (expectedAuthorTimestampOffsets.containsKey(relId)) { assertEquals(expectedAuthorTimestampOffsets.get(relId), n.getRev().getAuthorDateOffset()); } } } @Test public void testOrigins() { List expectedOris = List.of(new SWHID(TEST_ORIGIN_ID)); Map expectedUrls = Map.of(new SWHID(TEST_ORIGIN_ID), "https://example.com/swh/graph"); for (SWHID oriSwhid : expectedOris) { Node n = client.getNode(GetNodeRequest.newBuilder().setSwhid(oriSwhid.toString()).build()); assertTrue(n.hasOri()); assertTrue(n.getOri().hasUrl()); assertEquals(expectedUrls.get(oriSwhid), n.getOri().getUrl()); } } @Test public void testCntMask() { Node n; String swhid = fakeSWHID("cnt", 1).toString(); // No mask, all fields present n = client.getNode(GetNodeRequest.newBuilder().setSwhid(swhid).build()); assertTrue(n.hasCnt()); assertTrue(n.getCnt().hasLength()); assertEquals(42, n.getCnt().getLength()); assertTrue(n.getCnt().hasIsSkipped()); assertFalse(n.getCnt().getIsSkipped()); // Empty mask, no fields present n = client.getNode(GetNodeRequest.newBuilder().setSwhid(swhid).setMask(FieldMask.getDefaultInstance()).build()); assertFalse(n.getCnt().hasLength()); assertFalse(n.getCnt().hasIsSkipped()); // Mask with length, no isSkipped n = client.getNode(GetNodeRequest.newBuilder().setSwhid(swhid) .setMask(FieldMask.newBuilder().addPaths("cnt.length").build()).build()); assertTrue(n.getCnt().hasLength()); assertFalse(n.getCnt().hasIsSkipped()); // Mask with isSkipped, no length n = client.getNode(GetNodeRequest.newBuilder().setSwhid(swhid) .setMask(FieldMask.newBuilder().addPaths("cnt.is_skipped").build()).build()); assertFalse(n.getCnt().hasLength()); assertTrue(n.getCnt().hasIsSkipped()); } @Test public void testRevMask() { Node n; String swhid = fakeSWHID("rev", 3).toString(); // No mask, all fields present n = client.getNode(GetNodeRequest.newBuilder().setSwhid(swhid).build()); assertTrue(n.hasRev()); assertTrue(n.getRev().hasMessage()); assertTrue(n.getRev().hasAuthor()); assertTrue(n.getRev().hasAuthorDate()); assertTrue(n.getRev().hasAuthorDateOffset()); assertTrue(n.getRev().hasCommitter()); assertTrue(n.getRev().hasCommitterDate()); assertTrue(n.getRev().hasCommitterDateOffset()); // Empty mask, no fields present n = client.getNode(GetNodeRequest.newBuilder().setSwhid(swhid).setMask(FieldMask.getDefaultInstance()).build()); assertFalse(n.getRev().hasMessage()); assertFalse(n.getRev().hasAuthor()); assertFalse(n.getRev().hasAuthorDate()); assertFalse(n.getRev().hasAuthorDateOffset()); assertFalse(n.getRev().hasCommitter()); assertFalse(n.getRev().hasCommitterDate()); assertFalse(n.getRev().hasCommitterDateOffset()); // Test all masks with single fields for (Descriptors.FieldDescriptor includedField : RevisionData.getDefaultInstance().getAllFields().keySet()) { n = client.getNode(GetNodeRequest.newBuilder().setSwhid(swhid) .setMask(FieldMask.newBuilder().addPaths("rev." + includedField.getName()).build()).build()); for (Descriptors.FieldDescriptor f : n.getRev().getDescriptorForType().getFields()) { assertEquals(n.getRev().hasField(f), f.getName().equals(includedField.getName())); } } } @Test public void testRelMask() { Node n; String swhid = fakeSWHID("rel", 19).toString(); // No mask, all fields present n = client.getNode(GetNodeRequest.newBuilder().setSwhid(swhid).build()); assertTrue(n.hasRel()); assertTrue(n.getRel().hasMessage()); assertTrue(n.getRel().hasAuthor()); assertTrue(n.getRel().hasAuthorDate()); assertTrue(n.getRel().hasAuthorDateOffset()); // Empty mask, no fields present n = client.getNode(GetNodeRequest.newBuilder().setSwhid(swhid).setMask(FieldMask.getDefaultInstance()).build()); assertFalse(n.getRel().hasMessage()); assertFalse(n.getRel().hasAuthor()); assertFalse(n.getRel().hasAuthorDate()); assertFalse(n.getRel().hasAuthorDateOffset()); // Test all masks with single fields for (Descriptors.FieldDescriptor includedField : ReleaseData.getDefaultInstance().getAllFields().keySet()) { n = client.getNode(GetNodeRequest.newBuilder().setSwhid(swhid) .setMask(FieldMask.newBuilder().addPaths("rel." + includedField.getName()).build()).build()); for (Descriptors.FieldDescriptor f : n.getRel().getDescriptorForType().getFields()) { assertEquals(n.getRel().hasField(f), f.getName().equals(includedField.getName())); } } } @Test public void testOriMask() { Node n; String swhid = TEST_ORIGIN_ID; // No mask, all fields present n = client.getNode(GetNodeRequest.newBuilder().setSwhid(swhid).build()); assertTrue(n.hasOri()); assertTrue(n.getOri().hasUrl()); // Empty mask, no fields present n = client.getNode(GetNodeRequest.newBuilder().setSwhid(swhid).setMask(FieldMask.getDefaultInstance()).build()); assertFalse(n.getOri().hasUrl()); // Test all masks with single fields for (Descriptors.FieldDescriptor includedField : OriginData.getDefaultInstance().getAllFields().keySet()) { n = client.getNode(GetNodeRequest.newBuilder().setSwhid(swhid) .setMask(FieldMask.newBuilder().addPaths("ori." + includedField.getName()).build()).build()); for (Descriptors.FieldDescriptor f : n.getOri().getDescriptorForType().getFields()) { assertEquals(n.getOri().hasField(f), f.getName().equals(includedField.getName())); } } } } diff --git a/java/src/test/java/org/softwareheritage/graph/rpc/TraverseNodesTest.java b/java/src/test/java/org/softwareheritage/graph/rpc/TraverseNodesTest.java index 2dc34fd..433dedb 100644 --- a/java/src/test/java/org/softwareheritage/graph/rpc/TraverseNodesTest.java +++ b/java/src/test/java/org/softwareheritage/graph/rpc/TraverseNodesTest.java @@ -1,167 +1,196 @@ package org.softwareheritage.graph.rpc; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; import org.junit.jupiter.api.Test; import org.softwareheritage.graph.GraphTest; import org.softwareheritage.graph.SWHID; import java.util.ArrayList; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + public class TraverseNodesTest extends TraversalServiceTest { private TraversalRequest.Builder getTraversalRequestBuilder(SWHID src) { return TraversalRequest.newBuilder().addSrc(src.toString()); } + @Test + public void testSrcErrors() { + StatusRuntimeException thrown; + thrown = assertThrows(StatusRuntimeException.class, + () -> client.traverse(TraversalRequest.newBuilder().addSrc(fakeSWHID("cnt", 404).toString()).build()) + .forEachRemaining((n) -> { + })); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + thrown = assertThrows(StatusRuntimeException.class, + () -> client + .traverse(TraversalRequest.newBuilder() + .addSrc("swh:1:lol:0000000000000000000000000000000000000001").build()) + .forEachRemaining((n) -> { + })); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + thrown = assertThrows(StatusRuntimeException.class, + () -> client + .traverse(TraversalRequest.newBuilder() + .addSrc("swh:1:cnt:000000000000000000000000000000000000000z").build()) + .forEachRemaining((n) -> { + })); + assertEquals(Status.INVALID_ARGUMENT, thrown.getStatus()); + } + @Test public void forwardFromRoot() { ArrayList actual = getSWHIDs( client.traverse(getTraversalRequestBuilder(new SWHID(TEST_ORIGIN_ID)).build())); List expected = List.of(fakeSWHID("cnt", 1), fakeSWHID("cnt", 4), fakeSWHID("cnt", 5), fakeSWHID("cnt", 7), fakeSWHID("dir", 2), fakeSWHID("dir", 6), fakeSWHID("dir", 8), fakeSWHID("rel", 10), fakeSWHID("rev", 3), fakeSWHID("rev", 9), fakeSWHID("snp", 20), new SWHID(TEST_ORIGIN_ID)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void forwardFromMiddle() { ArrayList actual = getSWHIDs(client.traverse(getTraversalRequestBuilder(fakeSWHID("dir", 12)).build())); List expected = List.of(fakeSWHID("cnt", 1), fakeSWHID("cnt", 4), fakeSWHID("cnt", 5), fakeSWHID("cnt", 7), fakeSWHID("cnt", 11), fakeSWHID("dir", 6), fakeSWHID("dir", 8), fakeSWHID("dir", 12)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void forwardRelRev() { ArrayList actual = getSWHIDs( client.traverse(getTraversalRequestBuilder(fakeSWHID("rel", 10)).setEdges("rel:rev,rev:rev").build())); List expected = List.of(fakeSWHID("rel", 10), fakeSWHID("rev", 9), fakeSWHID("rev", 3)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void forwardFilterReturnedNodesDir() { ArrayList actual = getSWHIDs(client.traverse(getTraversalRequestBuilder(fakeSWHID("rel", 10)) .setReturnNodes(NodeFilter.newBuilder().setTypes("dir").build()).build())); List expected = List.of(fakeSWHID("dir", 2), fakeSWHID("dir", 8), fakeSWHID("dir", 6)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void backwardFromRoot() { ArrayList actual = getSWHIDs(client.traverse( getTraversalRequestBuilder(new SWHID(TEST_ORIGIN_ID)).setDirection(GraphDirection.BACKWARD).build())); List expected = List.of(new SWHID(TEST_ORIGIN_ID)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void backwardFromMiddle() { ArrayList actual = getSWHIDs(client.traverse( getTraversalRequestBuilder(fakeSWHID("dir", 12)).setDirection(GraphDirection.BACKWARD).build())); List expected = List.of(fakeSWHID("dir", 12), fakeSWHID("rel", 19), fakeSWHID("rev", 13), fakeSWHID("rev", 18)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void backwardFromLeaf() { ArrayList actual = getSWHIDs(client.traverse( getTraversalRequestBuilder(fakeSWHID("cnt", 4)).setDirection(GraphDirection.BACKWARD).build())); List expected = List.of(new SWHID(TEST_ORIGIN_ID), fakeSWHID("cnt", 4), fakeSWHID("dir", 6), fakeSWHID("dir", 8), fakeSWHID("dir", 12), fakeSWHID("rel", 10), fakeSWHID("rel", 19), fakeSWHID("rev", 9), fakeSWHID("rev", 13), fakeSWHID("rev", 18), fakeSWHID("snp", 20)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void forwardSnpToRev() { ArrayList actual = getSWHIDs( client.traverse(getTraversalRequestBuilder(fakeSWHID("snp", 20)).setEdges("snp:rev").build())); List expected = List.of(fakeSWHID("rev", 9), fakeSWHID("snp", 20)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void forwardRelToRevRevToRev() { ArrayList actual = getSWHIDs( client.traverse(getTraversalRequestBuilder(fakeSWHID("rel", 10)).setEdges("rel:rev,rev:rev").build())); List expected = List.of(fakeSWHID("rel", 10), fakeSWHID("rev", 3), fakeSWHID("rev", 9)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void forwardRevToAllDirToAll() { ArrayList actual = getSWHIDs( client.traverse(getTraversalRequestBuilder(fakeSWHID("rev", 13)).setEdges("rev:*,dir:*").build())); List expected = List.of(fakeSWHID("cnt", 1), fakeSWHID("cnt", 4), fakeSWHID("cnt", 5), fakeSWHID("cnt", 7), fakeSWHID("cnt", 11), fakeSWHID("dir", 2), fakeSWHID("dir", 6), fakeSWHID("dir", 8), fakeSWHID("dir", 12), fakeSWHID("rev", 3), fakeSWHID("rev", 9), fakeSWHID("rev", 13)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void forwardSnpToAllRevToAll() { ArrayList actual = getSWHIDs( client.traverse(getTraversalRequestBuilder(fakeSWHID("snp", 20)).setEdges("snp:*,rev:*").build())); List expected = List.of(fakeSWHID("dir", 2), fakeSWHID("dir", 8), fakeSWHID("rel", 10), fakeSWHID("rev", 3), fakeSWHID("rev", 9), fakeSWHID("snp", 20)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void forwardNoEdges() { ArrayList actual = getSWHIDs( client.traverse(getTraversalRequestBuilder(fakeSWHID("snp", 20)).setEdges("").build())); List expected = List.of(fakeSWHID("snp", 20)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void backwardRevToRevRevToRel() { ArrayList actual = getSWHIDs(client.traverse(getTraversalRequestBuilder(fakeSWHID("rev", 3)) .setEdges("rev:rev,rev:rel").setDirection(GraphDirection.BACKWARD).build())); List expected = List.of(fakeSWHID("rel", 10), fakeSWHID("rel", 19), fakeSWHID("rev", 3), fakeSWHID("rev", 9), fakeSWHID("rev", 13), fakeSWHID("rev", 18)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void forwardFromRootNodesOnly() { ArrayList actual = getSWHIDs( client.traverse(getTraversalRequestBuilder(new SWHID(TEST_ORIGIN_ID)).build())); List expected = List.of(new SWHID(TEST_ORIGIN_ID), fakeSWHID("cnt", 1), fakeSWHID("cnt", 4), fakeSWHID("cnt", 5), fakeSWHID("cnt", 7), fakeSWHID("dir", 2), fakeSWHID("dir", 6), fakeSWHID("dir", 8), fakeSWHID("rel", 10), fakeSWHID("rev", 3), fakeSWHID("rev", 9), fakeSWHID("snp", 20)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void backwardRevToAllNodesOnly() { ArrayList actual = getSWHIDs(client.traverse(getTraversalRequestBuilder(fakeSWHID("rev", 3)) .setDirection(GraphDirection.BACKWARD).setEdges("rev:*").build())); List expected = List.of(fakeSWHID("rel", 10), fakeSWHID("rel", 19), fakeSWHID("rev", 3), fakeSWHID("rev", 9), fakeSWHID("rev", 13), fakeSWHID("rev", 18), fakeSWHID("snp", 20)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void forwardMultipleSources() { ArrayList actual = getSWHIDs(client.traverse(getTraversalRequestBuilder(fakeSWHID("snp", 20)) .addSrc(fakeSWHID("rel", 19).toString()).setMaxDepth(1).build())); List expected = List.of(fakeSWHID("snp", 20), fakeSWHID("rel", 19), fakeSWHID("rel", 10), fakeSWHID("rev", 9), fakeSWHID("rev", 18)); GraphTest.assertEqualsAnyOrder(expected, actual); } @Test public void backwardMultipleSources() { ArrayList actual = getSWHIDs(client.traverse(getTraversalRequestBuilder(fakeSWHID("cnt", 5)) .addSrc(fakeSWHID("dir", 16).toString()).setMaxDepth(2).setDirection(GraphDirection.BACKWARD).build())); List expected = List.of(fakeSWHID("cnt", 5), fakeSWHID("dir", 16), fakeSWHID("dir", 6), fakeSWHID("dir", 8), fakeSWHID("dir", 17), fakeSWHID("rev", 18)); GraphTest.assertEqualsAnyOrder(expected, actual); } }