Skip to content

Commit 8e38bb9

Browse files
committed
test(rpc): cover the error path, cursor wiring and pbft reads
Three gaps the existing suite left open: RpcApiServiceErrorPathTest drives every unary handler of WalletApi and WalletSolidityApi with collaborators that throw, and asserts none of them terminates the call more than once. Reverting the return fix makes it fail on getDelegatedResourceV2, getPendingSize and getBlock, so it also reaches the shared *Common helpers. CursorInterceptorWiringTest runs the real addService of the solidity and pbft services against a mock builder and asserts both shared read services are registered as intercepted definitions. Dropping ServerInterceptors.intercept left every existing test green while the port silently served HEAD; this is the grpc counterpart of CursorFilterInstallationTest. RpcApiServicesTest now drives getPaginatedNowWitnessList and getTransactionInfoByBlockNum on the pbft stub as well, pinning the one intentional behaviour change of the merge.
1 parent 726c428 commit 8e38bb9

3 files changed

Lines changed: 243 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package org.tron.core.services;
2+
3+
import static org.mockito.Mockito.mock;
4+
import static org.mockito.Mockito.withSettings;
5+
6+
import com.google.protobuf.Message;
7+
import io.grpc.stub.StreamObserver;
8+
import java.lang.reflect.Field;
9+
import java.lang.reflect.InvocationTargetException;
10+
import java.lang.reflect.Method;
11+
import java.lang.reflect.Modifier;
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import org.junit.Assert;
15+
import org.junit.Test;
16+
import org.mockito.Answers;
17+
import org.mockito.stubbing.Answer;
18+
import org.tron.core.Wallet;
19+
import org.tron.core.metrics.MetricsApiService;
20+
import org.tron.core.services.RpcApiService.WalletApi;
21+
import org.tron.core.services.RpcApiService.WalletSolidityApi;
22+
import org.tron.core.utils.TransactionUtil;
23+
24+
/**
25+
* Pins the one-terminal-event rule on the gRPC error path: a handler that reports a failure through
26+
* {@code onError} must not fall through to {@code onCompleted}. gRPC rejects the second close with
27+
* {@code IllegalStateException("call already closed")}, so a handler doing both costs a server-side
28+
* exception on every failed call while the client sees nothing extra.
29+
*
30+
* <p>The rule is checked for every handler rather than for the ones that were fixed, because the
31+
* shape is trivially reintroduced by copying a neighbouring handler.
32+
*/
33+
public class RpcApiServiceErrorPathTest {
34+
35+
/** Minimum handlers that must actually fail, so the sweep cannot silently cover nothing. */
36+
private static final int MIN_EXERCISED = 20;
37+
38+
@Test
39+
public void testWalletApiTerminatesTheCallOnce() throws Exception {
40+
assertSingleTerminalEvent(WalletApi.class);
41+
}
42+
43+
@Test
44+
public void testWalletSolidityApiTerminatesTheCallOnce() throws Exception {
45+
assertSingleTerminalEvent(WalletSolidityApi.class);
46+
}
47+
48+
/**
49+
* Drives every unary handler of the given service class with collaborators that throw, and
50+
* asserts none of them terminates the call more than once.
51+
*/
52+
private static void assertSingleTerminalEvent(Class<?> apiClass) throws Exception {
53+
RpcApiService service = mock(RpcApiService.class,
54+
withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS));
55+
injectThrowingCollaborators(service);
56+
Object api = apiClass.getDeclaredConstructor(RpcApiService.class).newInstance(service);
57+
58+
int exercised = 0;
59+
for (Method method : apiClass.getDeclaredMethods()) {
60+
if (!isUnaryHandler(method)) {
61+
continue;
62+
}
63+
Message request = (Message) method.getParameterTypes()[0]
64+
.getMethod("getDefaultInstance").invoke(null);
65+
TerminalRecorder recorder = new TerminalRecorder();
66+
try {
67+
method.invoke(api, request, recorder);
68+
} catch (InvocationTargetException e) {
69+
// a handler that lets the failure escape cannot have closed the call twice
70+
continue;
71+
}
72+
Assert.assertTrue(
73+
method.getName() + " terminated the call " + recorder.events.size() + " times "
74+
+ recorder.events + "; onError must be followed by return",
75+
recorder.events.size() <= 1);
76+
if (!recorder.events.isEmpty()) {
77+
exercised++;
78+
}
79+
}
80+
Assert.assertTrue(
81+
apiClass.getSimpleName() + " exercised only " + exercised + " handlers, expected at least "
82+
+ MIN_EXERCISED + " — the sweep is no longer reaching the handler bodies",
83+
exercised >= MIN_EXERCISED);
84+
}
85+
86+
private static boolean isUnaryHandler(Method method) {
87+
Class<?>[] params = method.getParameterTypes();
88+
return Modifier.isPublic(method.getModifiers())
89+
&& method.getReturnType() == void.class
90+
&& params.length == 2
91+
&& Message.class.isAssignableFrom(params[0])
92+
&& params[1] == StreamObserver.class;
93+
}
94+
95+
/**
96+
* Replaces the service's collaborators with mocks that throw on every call, so each handler takes
97+
* its own error path, and binds a real {@code WalletApi} for the solidity handlers to delegate
98+
* to.
99+
*/
100+
private static void injectThrowingCollaborators(RpcApiService service) throws Exception {
101+
Answer<Object> throwing = invocation -> {
102+
throw new RuntimeException("collaborator unavailable");
103+
};
104+
set(service, "wallet", mock(Wallet.class, withSettings().defaultAnswer(throwing)));
105+
set(service, "transactionUtil",
106+
mock(TransactionUtil.class, withSettings().defaultAnswer(throwing)));
107+
set(service, "nodeInfoService",
108+
mock(NodeInfoService.class, withSettings().defaultAnswer(throwing)));
109+
set(service, "metricsApiService",
110+
mock(MetricsApiService.class, withSettings().defaultAnswer(throwing)));
111+
set(service, "walletApi",
112+
WalletApi.class.getDeclaredConstructor(RpcApiService.class).newInstance(service));
113+
}
114+
115+
private static void set(RpcApiService service, String name, Object value) throws Exception {
116+
Field field = RpcApiService.class.getDeclaredField(name);
117+
field.setAccessible(true);
118+
field.set(service, value);
119+
}
120+
121+
/** Counts terminal events instead of closing a real call. */
122+
private static final class TerminalRecorder implements StreamObserver<Object> {
123+
124+
private final List<String> events = new ArrayList<>();
125+
126+
@Override
127+
public void onNext(Object value) {
128+
}
129+
130+
@Override
131+
public void onError(Throwable t) {
132+
events.add("onError");
133+
}
134+
135+
@Override
136+
public void onCompleted() {
137+
events.add("onCompleted");
138+
}
139+
}
140+
}

‎framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ public void testGetPaginatedNowWitnessList() {
277277
.setOffset(0).setLimit(5).build();
278278
assertNotNull(blockingStubFull.getPaginatedNowWitnessList(paginatedMessage));
279279
assertNotNull(blockingStubSolidity.getPaginatedNowWitnessList(paginatedMessage));
280+
assertNotNull(blockingStubPBFT.getPaginatedNowWitnessList(paginatedMessage));
280281
}
281282

282283
@Test
@@ -673,6 +674,7 @@ public void testGetTransactionInfoByBlockNum() {
673674
NumberMessage message = NumberMessage.newBuilder().setNum(1).build();
674675
assertNotNull(blockingStubFull.getTransactionInfoByBlockNum(message));
675676
assertNotNull(blockingStubSolidity.getTransactionInfoByBlockNum(message));
677+
assertNotNull(blockingStubPBFT.getTransactionInfoByBlockNum(message));
676678
}
677679

678680
@Test
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package org.tron.core.services.filter;
2+
3+
import static org.mockito.ArgumentMatchers.any;
4+
import static org.mockito.BDDMockito.given;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.never;
7+
import static org.mockito.Mockito.times;
8+
import static org.mockito.Mockito.verify;
9+
import static org.mockito.Mockito.withSettings;
10+
11+
import io.grpc.BindableService;
12+
import io.grpc.ServerServiceDefinition;
13+
import io.grpc.netty.NettyServerBuilder;
14+
import java.lang.reflect.Field;
15+
import java.lang.reflect.Method;
16+
import java.util.Arrays;
17+
import java.util.HashSet;
18+
import java.util.Set;
19+
import org.junit.Assert;
20+
import org.junit.Test;
21+
import org.mockito.Answers;
22+
import org.mockito.ArgumentCaptor;
23+
import org.tron.api.DatabaseGrpc;
24+
import org.tron.api.WalletSolidityGrpc;
25+
import org.tron.core.services.RpcApiService;
26+
import org.tron.core.services.interfaceOnPBFT.RpcApiServiceOnPBFT;
27+
import org.tron.core.services.interfaceOnSolidity.RpcApiServiceOnSolidity;
28+
29+
/**
30+
* Guards that each cursor gRPC service registers the shared read services <em>through</em> its
31+
* cursor interceptor. Nothing else catches a dropped interceptor: the services would still be
32+
* served and every response would still look well-formed, only resolved against HEAD instead of the
33+
* solidified or PBFT snapshot. This is the gRPC counterpart of CursorFilterInstallationTest.
34+
*
35+
* <p>Registering without the interceptor binds the {@code addService(BindableService)} overload
36+
* rather than the {@code addService(ServerServiceDefinition)} one, so the two are distinguishable
37+
* here. What the interceptor does once attached is pinned by CursorInterceptorScopeTest and
38+
* GrpcInterceptorProbeTest.
39+
*/
40+
public class CursorInterceptorWiringTest {
41+
42+
private static final Set<String> SHARED_READ_SERVICES = new HashSet<>(
43+
Arrays.asList(DatabaseGrpc.SERVICE_NAME, WalletSolidityGrpc.SERVICE_NAME));
44+
45+
@Test
46+
public void testSolidityServiceRegistersBothReadServicesThroughTheCursor() throws Exception {
47+
Assert.assertEquals(SHARED_READ_SERVICES,
48+
interceptedServices(RpcApiServiceOnSolidity.class, new SolidityCursorInterceptor()));
49+
}
50+
51+
@Test
52+
public void testPbftServiceRegistersBothReadServicesThroughTheCursor() throws Exception {
53+
Assert.assertEquals(SHARED_READ_SERVICES,
54+
interceptedServices(RpcApiServiceOnPBFT.class, new PbftCursorInterceptor()));
55+
}
56+
57+
/**
58+
* Runs the service's real addService against a mock builder and returns the names of the services
59+
* it registered as intercepted definitions, failing if any was registered unintercepted.
60+
*/
61+
private static Set<String> interceptedServices(Class<?> serviceClass,
62+
CursorServerInterceptor interceptor) throws Exception {
63+
RpcApiService rpcApiService = mock(RpcApiService.class);
64+
given(rpcApiService.getDatabaseApi()).willReturn(RpcApiService.DatabaseApi.class
65+
.getDeclaredConstructor(RpcApiService.class).newInstance(rpcApiService));
66+
given(rpcApiService.getWalletSolidityApi()).willReturn(RpcApiService.WalletSolidityApi.class
67+
.getDeclaredConstructor(RpcApiService.class).newInstance(rpcApiService));
68+
69+
Object service = mock(serviceClass, withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS));
70+
inject(serviceClass, service, rpcApiService);
71+
inject(serviceClass, service, interceptor);
72+
73+
NettyServerBuilder builder = mock(NettyServerBuilder.class);
74+
Method addService = serviceClass.getDeclaredMethod("addService", NettyServerBuilder.class);
75+
addService.setAccessible(true);
76+
addService.invoke(service, builder);
77+
78+
verify(builder, never()).addService(any(BindableService.class));
79+
ArgumentCaptor<ServerServiceDefinition> registered =
80+
ArgumentCaptor.forClass(ServerServiceDefinition.class);
81+
verify(builder, times(2)).addService(registered.capture());
82+
83+
Set<String> names = new HashSet<>();
84+
for (ServerServiceDefinition definition : registered.getAllValues()) {
85+
names.add(definition.getServiceDescriptor().getName());
86+
}
87+
return names;
88+
}
89+
90+
/** Sets the one declared field the value fits; the two injected types are unrelated. */
91+
private static void inject(Class<?> serviceClass, Object service, Object value) throws Exception {
92+
for (Field field : serviceClass.getDeclaredFields()) {
93+
if (field.getType().isInstance(value)) {
94+
field.setAccessible(true);
95+
field.set(service, value);
96+
return;
97+
}
98+
}
99+
Assert.fail(serviceClass.getSimpleName() + " has no field for " + value.getClass().getName());
100+
}
101+
}

0 commit comments

Comments
 (0)