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 b7399b0..3719ec5 100644 --- a/java/src/main/java/org/softwareheritage/graph/rpc/GraphServer.java +++ b/java/src/main/java/org/softwareheritage/graph/rpc/GraphServer.java @@ -1,260 +1,274 @@ 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; +import java.util.concurrent.atomic.AtomicLong; /** * 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 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.withDescription(e.getMessage()).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(); Traversal.SimpleTraversal t; try { t = new Traversal.SimpleTraversal(g, request, responseObserver::onNext); } catch (IllegalArgumentException e) { responseObserver .onError(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).withCause(e).asException()); return; } t.visit(); responseObserver.onCompleted(); } @Override public void findPathTo(FindPathToRequest request, StreamObserver responseObserver) { SwhBidirectionalGraph g = graph.copy(); Traversal.FindPathTo t; try { t = new Traversal.FindPathTo(g, request); } catch (IllegalArgumentException e) { responseObserver .onError(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).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(); Traversal.FindPathBetween t; try { t = new Traversal.FindPathBetween(g, request); } catch (IllegalArgumentException e) { responseObserver .onError(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).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); + AtomicLong count = new AtomicLong(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()); + Traversal.SimpleTraversal t; + try { + t = new Traversal.SimpleTraversal(g, fixedReq, n -> count.incrementAndGet()); + } catch (IllegalArgumentException e) { + responseObserver + .onError(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).withCause(e).asException()); + return; + } 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); + AtomicLong count = new AtomicLong(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())); + .setMask(FieldMask.newBuilder().addPaths("num_successors").build()).build(); + Traversal.SimpleTraversal t; + try { + t = new Traversal.SimpleTraversal(g, fixedReq, n -> count.addAndGet(n.getNumSuccessors())); + } catch (IllegalArgumentException e) { + responseObserver + .onError(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).withCause(e).asException()); + return; + } t.visit(); CountResponse response = CountResponse.newBuilder().setCount(count.get()).build(); responseObserver.onNext(response); responseObserver.onCompleted(); } } } diff --git a/java/src/main/java/org/softwareheritage/graph/rpc/NodePropertyBuilder.java b/java/src/main/java/org/softwareheritage/graph/rpc/NodePropertyBuilder.java index 8a13e2b..c22563b 100644 --- a/java/src/main/java/org/softwareheritage/graph/rpc/NodePropertyBuilder.java +++ b/java/src/main/java/org/softwareheritage/graph/rpc/NodePropertyBuilder.java @@ -1,184 +1,193 @@ /* * Copyright (c) 2022 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU General Public License version 3, or any later version * See top-level LICENSE file for more information */ package org.softwareheritage.graph.rpc; import com.google.protobuf.ByteString; import com.google.protobuf.FieldMask; import com.google.protobuf.util.FieldMaskUtil; import it.unimi.dsi.big.webgraph.labelling.Label; import org.softwareheritage.graph.SwhUnidirectionalGraph; import org.softwareheritage.graph.labels.DirEntry; import java.util.*; public class NodePropertyBuilder { public static class NodeDataMask { public boolean swhid; public boolean successor; public boolean successorSwhid; public boolean successorLabel; + public boolean numSuccessors; public boolean cntLength; public boolean cntIsSkipped; public boolean revAuthor; public boolean revAuthorDate; public boolean revAuthorDateOffset; public boolean revCommitter; public boolean revCommitterDate; public boolean revCommitterDateOffset; public boolean revMessage; public boolean relAuthor; public boolean relAuthorDate; public boolean relAuthorDateOffset; public boolean relName; public boolean relMessage; public boolean oriUrl; public NodeDataMask(FieldMask mask) { Set allowedFields = null; if (mask != null) { mask = FieldMaskUtil.normalize(mask); allowedFields = new HashSet<>(mask.getPathsList()); } this.swhid = allowedFields == null || allowedFields.contains("swhid"); this.successorSwhid = allowedFields == null || allowedFields.contains("successor") || allowedFields.contains("successor.swhid"); this.successorLabel = allowedFields == null || allowedFields.contains("successor") || allowedFields.contains("successor.label"); this.successor = this.successorSwhid || this.successorLabel; + this.numSuccessors = allowedFields == null || allowedFields.contains("num_successors"); this.cntLength = allowedFields == null || allowedFields.contains("cnt.length"); this.cntIsSkipped = allowedFields == null || allowedFields.contains("cnt.is_skipped"); this.revAuthor = allowedFields == null || allowedFields.contains("rev.author"); this.revAuthorDate = allowedFields == null || allowedFields.contains("rev.author_date"); this.revAuthorDateOffset = allowedFields == null || allowedFields.contains("rev.author_date_offset"); this.revCommitter = allowedFields == null || allowedFields.contains("rev.committer"); this.revCommitterDate = allowedFields == null || allowedFields.contains("rev.committer_date"); this.revCommitterDateOffset = allowedFields == null || allowedFields.contains("rev.committer_date_offset"); this.revMessage = allowedFields == null || allowedFields.contains("rev.message"); this.relAuthor = allowedFields == null || allowedFields.contains("rel.author"); this.relAuthorDate = allowedFields == null || allowedFields.contains("rel.author_date"); this.relAuthorDateOffset = allowedFields == null || allowedFields.contains("rel.author_date_offset"); this.relName = allowedFields == null || allowedFields.contains("rel.name"); this.relMessage = allowedFields == null || allowedFields.contains("rel.message"); this.oriUrl = allowedFields == null || allowedFields.contains("ori.url"); } } public static void buildNodeProperties(SwhUnidirectionalGraph graph, NodeDataMask mask, Node.Builder nodeBuilder, long node) { if (mask.swhid) { nodeBuilder.setSwhid(graph.getSWHID(node).toString()); } switch (graph.getNodeType(node)) { case CNT: ContentData.Builder cntBuilder = ContentData.newBuilder(); if (mask.cntLength) { cntBuilder.setLength(graph.getContentLength(node)); } if (mask.cntIsSkipped) { cntBuilder.setIsSkipped(graph.isContentSkipped(node)); } nodeBuilder.setCnt(cntBuilder.build()); break; case REV: RevisionData.Builder revBuilder = RevisionData.newBuilder(); if (mask.revAuthor) { revBuilder.setAuthor(graph.getAuthorId(node)); } if (mask.revAuthorDate) { revBuilder.setAuthorDate(graph.getAuthorTimestamp(node)); } if (mask.revAuthorDateOffset) { revBuilder.setAuthorDateOffset(graph.getAuthorTimestampOffset(node)); } if (mask.revCommitter) { revBuilder.setCommitter(graph.getCommitterId(node)); } if (mask.revCommitterDate) { revBuilder.setCommitterDate(graph.getCommitterTimestamp(node)); } if (mask.revCommitterDateOffset) { revBuilder.setCommitterDateOffset(graph.getCommitterTimestampOffset(node)); } if (mask.revMessage) { byte[] msg = graph.getMessage(node); if (msg != null) { revBuilder.setMessage(ByteString.copyFrom(msg)); } } nodeBuilder.setRev(revBuilder.build()); break; case REL: ReleaseData.Builder relBuilder = ReleaseData.newBuilder(); if (mask.relAuthor) { relBuilder.setAuthor(graph.getAuthorId(node)); } if (mask.relAuthorDate) { relBuilder.setAuthorDate(graph.getAuthorTimestamp(node)); } if (mask.relAuthorDateOffset) { relBuilder.setAuthorDateOffset(graph.getAuthorTimestampOffset(node)); } if (mask.relName) { byte[] msg = graph.getMessage(node); if (msg != null) { relBuilder.setMessage(ByteString.copyFrom(msg)); } } if (mask.relMessage) { byte[] msg = graph.getMessage(node); if (msg != null) { relBuilder.setMessage(ByteString.copyFrom(msg)); } } nodeBuilder.setRel(relBuilder.build()); break; case ORI: OriginData.Builder oriBuilder = OriginData.newBuilder(); if (mask.oriUrl) { String url = graph.getUrl(node); if (url != null) { oriBuilder.setUrl(url); } } nodeBuilder.setOri(oriBuilder.build()); } } public static void buildNodeProperties(SwhUnidirectionalGraph graph, FieldMask mask, Node.Builder nodeBuilder, long node) { NodeDataMask nodeMask = new NodeDataMask(mask); buildNodeProperties(graph, nodeMask, nodeBuilder, node); } public static void buildSuccessorProperties(SwhUnidirectionalGraph graph, NodeDataMask mask, Node.Builder nodeBuilder, long src, long dst, Label label) { - if (nodeBuilder != null && mask.successor) { + if (nodeBuilder != null) { Successor.Builder successorBuilder = Successor.newBuilder(); if (mask.successorSwhid) { successorBuilder.setSwhid(graph.getSWHID(dst).toString()); } if (mask.successorLabel) { DirEntry[] entries = (DirEntry[]) label.get(); for (DirEntry entry : entries) { EdgeLabel.Builder builder = EdgeLabel.newBuilder(); builder.setName(ByteString.copyFrom(graph.getLabelName(entry.filenameId))); builder.setPermission(entry.permission); successorBuilder.addLabel(builder.build()); } } - nodeBuilder.addSuccessor(successorBuilder.build()); + Successor successor = successorBuilder.build(); + if (successor != Successor.getDefaultInstance()) { + nodeBuilder.addSuccessor(successor); + } + + if (mask.numSuccessors) { + nodeBuilder.setNumSuccessors(nodeBuilder.getNumSuccessors() + 1); + } } } public static void buildSuccessorProperties(SwhUnidirectionalGraph graph, FieldMask mask, Node.Builder nodeBuilder, long src, long dst, Label label) { NodeDataMask nodeMask = new NodeDataMask(mask); buildSuccessorProperties(graph, nodeMask, nodeBuilder, src, dst, label); } } diff --git a/java/src/test/java/org/softwareheritage/graph/rpc/CountEdgesTest.java b/java/src/test/java/org/softwareheritage/graph/rpc/CountEdgesTest.java new file mode 100644 index 0000000..7445671 --- /dev/null +++ b/java/src/test/java/org/softwareheritage/graph/rpc/CountEdgesTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2022 The Software Heritage developers + * See the AUTHORS file at the top-level directory of this distribution + * License: GNU General Public License version 3, or any later version + * See top-level LICENSE file for more information + */ + +package org.softwareheritage.graph.rpc; + +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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CountEdgesTest extends TraversalServiceTest { + private TraversalRequest.Builder getTraversalRequestBuilder(SWHID src) { + return TraversalRequest.newBuilder().addSrc(src.toString()); + } + + @Test + public void testSwhidErrors() { + StatusRuntimeException thrown; + thrown = assertThrows(StatusRuntimeException.class, () -> client + .countEdges(TraversalRequest.newBuilder().addSrc(fakeSWHID("cnt", 404).toString()).build())); + assertEquals(Status.INVALID_ARGUMENT.getCode(), thrown.getStatus().getCode()); + thrown = assertThrows(StatusRuntimeException.class, () -> client.countEdges( + TraversalRequest.newBuilder().addSrc("swh:1:lol:0000000000000000000000000000000000000001").build())); + assertEquals(Status.INVALID_ARGUMENT.getCode(), thrown.getStatus().getCode()); + thrown = assertThrows(StatusRuntimeException.class, () -> client.countEdges( + TraversalRequest.newBuilder().addSrc("swh:1:cnt:000000000000000000000000000000000000000z").build())); + assertEquals(Status.INVALID_ARGUMENT.getCode(), thrown.getStatus().getCode()); + } + + @Test + public void forwardFromRoot() { + CountResponse actual = client.countEdges(getTraversalRequestBuilder(new SWHID(TEST_ORIGIN_ID)).build()); + assertEquals(13, actual.getCount()); + } + + @Test + public void forwardFromMiddle() { + CountResponse actual = client.countEdges(getTraversalRequestBuilder(fakeSWHID("dir", 12)).build()); + assertEquals(7, actual.getCount()); + } + + @Test + public void forwardRelRev() { + CountResponse actual = client + .countEdges(getTraversalRequestBuilder(fakeSWHID("rel", 10)).setEdges("rel:rev,rev:rev").build()); + assertEquals(2, actual.getCount()); + } + + @Test + public void backwardFromMiddle() { + CountResponse actual = client.countEdges( + getTraversalRequestBuilder(fakeSWHID("dir", 12)).setDirection(GraphDirection.BACKWARD).build()); + assertEquals(3, actual.getCount()); + } + + @Test + public void backwardFromLeaf() { + CountResponse actual = client.countEdges( + getTraversalRequestBuilder(fakeSWHID("cnt", 4)).setDirection(GraphDirection.BACKWARD).build()); + assertEquals(12, actual.getCount()); + } + + @Test + public void backwardRevToRevRevToRel() { + CountResponse actual = client.countEdges(getTraversalRequestBuilder(fakeSWHID("rev", 3)) + .setEdges("rev:rev,rev:rel").setDirection(GraphDirection.BACKWARD).build()); + assertEquals(5, actual.getCount()); + } + + @Test + public void testWithEmptyMask() { + CountResponse actual = client.countEdges( + getTraversalRequestBuilder(fakeSWHID("dir", 12)).setMask(FieldMask.getDefaultInstance()).build()); + assertEquals(7, actual.getCount()); + } +} diff --git a/java/src/test/java/org/softwareheritage/graph/rpc/CountNodesTest.java b/java/src/test/java/org/softwareheritage/graph/rpc/CountNodesTest.java new file mode 100644 index 0000000..a0bebc1 --- /dev/null +++ b/java/src/test/java/org/softwareheritage/graph/rpc/CountNodesTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2022 The Software Heritage developers + * See the AUTHORS file at the top-level directory of this distribution + * License: GNU General Public License version 3, or any later version + * See top-level LICENSE file for more information + */ + +package org.softwareheritage.graph.rpc; + +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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CountNodesTest extends TraversalServiceTest { + private TraversalRequest.Builder getTraversalRequestBuilder(SWHID src) { + return TraversalRequest.newBuilder().addSrc(src.toString()); + } + + @Test + public void testSwhidErrors() { + StatusRuntimeException thrown; + thrown = assertThrows(StatusRuntimeException.class, () -> client + .countNodes(TraversalRequest.newBuilder().addSrc(fakeSWHID("cnt", 404).toString()).build())); + assertEquals(Status.INVALID_ARGUMENT.getCode(), thrown.getStatus().getCode()); + thrown = assertThrows(StatusRuntimeException.class, () -> client.countNodes( + TraversalRequest.newBuilder().addSrc("swh:1:lol:0000000000000000000000000000000000000001").build())); + assertEquals(Status.INVALID_ARGUMENT.getCode(), thrown.getStatus().getCode()); + thrown = assertThrows(StatusRuntimeException.class, () -> client.countNodes( + TraversalRequest.newBuilder().addSrc("swh:1:cnt:000000000000000000000000000000000000000z").build())); + assertEquals(Status.INVALID_ARGUMENT.getCode(), thrown.getStatus().getCode()); + } + + @Test + public void forwardFromRoot() { + CountResponse actual = client.countNodes(getTraversalRequestBuilder(new SWHID(TEST_ORIGIN_ID)).build()); + assertEquals(12, actual.getCount()); + } + + @Test + public void forwardFromMiddle() { + CountResponse actual = client.countNodes(getTraversalRequestBuilder(fakeSWHID("dir", 12)).build()); + assertEquals(8, actual.getCount()); + } + + @Test + public void forwardRelRev() { + CountResponse actual = client + .countNodes(getTraversalRequestBuilder(fakeSWHID("rel", 10)).setEdges("rel:rev,rev:rev").build()); + assertEquals(3, actual.getCount()); + } + + @Test + public void backwardFromMiddle() { + CountResponse actual = client.countNodes( + getTraversalRequestBuilder(fakeSWHID("dir", 12)).setDirection(GraphDirection.BACKWARD).build()); + assertEquals(4, actual.getCount()); + } + + @Test + public void backwardFromLeaf() { + CountResponse actual = client.countNodes( + getTraversalRequestBuilder(fakeSWHID("cnt", 4)).setDirection(GraphDirection.BACKWARD).build()); + assertEquals(11, actual.getCount()); + } + + @Test + public void backwardRevToRevRevToRel() { + CountResponse actual = client.countNodes(getTraversalRequestBuilder(fakeSWHID("rev", 3)) + .setEdges("rev:rev,rev:rel").setDirection(GraphDirection.BACKWARD).build()); + assertEquals(6, actual.getCount()); + } + + @Test + public void testWithEmptyMask() { + CountResponse actual = client.countNodes( + getTraversalRequestBuilder(fakeSWHID("dir", 12)).setMask(FieldMask.getDefaultInstance()).build()); + assertEquals(8, actual.getCount()); + } +} diff --git a/proto/swhgraph.proto b/proto/swhgraph.proto index 87da0cb..192e7ff 100644 --- a/proto/swhgraph.proto +++ b/proto/swhgraph.proto @@ -1,145 +1,146 @@ syntax = "proto3"; import "google/protobuf/field_mask.proto"; option java_multiple_files = true; option java_package = "org.softwareheritage.graph.rpc"; option java_outer_classname = "GraphService"; package swh.graph; service TraversalService { rpc Traverse (TraversalRequest) returns (stream Node); rpc FindPathTo (FindPathToRequest) returns (Path); rpc FindPathBetween (FindPathBetweenRequest) returns (Path); rpc CountNodes (TraversalRequest) returns (CountResponse); rpc CountEdges (TraversalRequest) returns (CountResponse); rpc Stats (StatsRequest) returns (StatsResponse); rpc GetNode (GetNodeRequest) returns (Node); } enum GraphDirection { FORWARD = 0; BACKWARD = 1; } message TraversalRequest { repeated string src = 1; GraphDirection direction = 2; optional string edges = 3; optional int64 max_edges = 4; optional int64 min_depth = 5; optional int64 max_depth = 6; optional NodeFilter return_nodes = 7; optional google.protobuf.FieldMask mask = 8; } message FindPathToRequest { repeated string src = 1; optional NodeFilter target = 2; GraphDirection direction = 3; optional string edges = 4; optional int64 max_edges = 5; optional int64 max_depth = 6; optional google.protobuf.FieldMask mask = 7; } message FindPathBetweenRequest { repeated string src = 1; repeated string dst = 2; GraphDirection direction = 3; optional GraphDirection direction_reverse = 4; optional string edges = 5; optional string edges_reverse = 6; optional int64 max_edges = 7; optional int64 max_depth = 8; optional google.protobuf.FieldMask mask = 9; } message NodeFilter { optional string types = 1; optional int64 min_traversal_successors = 2; optional int64 max_traversal_successors = 3; } message Node { string swhid = 1; repeated Successor successor = 2; oneof data { ContentData cnt = 3; RevisionData rev = 5; ReleaseData rel = 6; OriginData ori = 8; }; + optional int64 num_successors = 9; } message Path { repeated Node node = 1; optional int64 middle_node_index = 2; } message Successor { optional string swhid = 1; repeated EdgeLabel label = 2; } message ContentData { optional int64 length = 1; optional bool is_skipped = 2; } message RevisionData { optional int64 author = 1; optional int64 author_date = 2; optional int32 author_date_offset = 3; optional int64 committer = 4; optional int64 committer_date = 5; optional int32 committer_date_offset = 6; optional bytes message = 7; } message ReleaseData { optional int64 author = 1; optional int64 author_date = 2; optional int32 author_date_offset = 3; optional bytes name = 4; optional bytes message = 5; } message OriginData { optional string url = 1; } message EdgeLabel { bytes name = 1; int32 permission = 2; } message CountResponse { int64 count = 1; } message StatsRequest { } message StatsResponse { int64 num_nodes = 1; int64 num_edges = 2; double compression = 3; double bits_per_node = 4; double bits_per_edge = 5; double avg_locality = 6; int64 indegree_min = 7; int64 indegree_max = 8; double indegree_avg = 9; int64 outdegree_min = 10; int64 outdegree_max = 11; double outdegree_avg = 12; } message GetNodeRequest { string swhid = 1; optional google.protobuf.FieldMask mask = 8; }