Skip to content

Commit 0235e9a

Browse files
authored
Merge pull request #73 from salesforce/feature/testing-rules
Added GrpcContextRule and NettyGrpcServerRule
2 parents 1db1a01 + b4247b9 commit 0235e9a

File tree

8 files changed

+465
-14
lines changed

8 files changed

+465
-14
lines changed

grpc-contrib/src/main/java/com/salesforce/grpc/contrib/Servers.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,28 @@ public final class Servers {
2828
* @throws InterruptedException if waiting for termination is interrupted
2929
*/
3030
public static Server shutdownGracefully(Server server, long maxWaitTimeInMillis) throws InterruptedException {
31+
return shutdownGracefully(server, maxWaitTimeInMillis, TimeUnit.MILLISECONDS);
32+
}
33+
34+
/**
35+
* Attempt to {@link Server#shutdown()} the {@link Server} gracefully. If the max wait time is exceeded, give up and
36+
* perform a hard {@link Server#shutdownNow()}.
37+
*
38+
* @param server the server to be shutdown
39+
* @param timeout the max amount of time to wait for graceful shutdown to occur
40+
* @param unit the time unit denominating the shutdown timeout
41+
* @return the given server
42+
* @throws InterruptedException if waiting for termination is interrupted
43+
*/
44+
public static Server shutdownGracefully(Server server, long timeout, TimeUnit unit) throws InterruptedException {
3145
Preconditions.checkNotNull(server, "server");
32-
Preconditions.checkArgument(maxWaitTimeInMillis > 0, "maxWaitTimeInMillis must be greater than 0");
46+
Preconditions.checkArgument(timeout > 0, "timeout must be greater than 0");
47+
Preconditions.checkNotNull(unit, "unit");
3348

3449
server.shutdown();
3550

3651
try {
37-
server.awaitTermination(maxWaitTimeInMillis, TimeUnit.MILLISECONDS);
52+
server.awaitTermination(timeout, unit);
3853
} finally {
3954
server.shutdownNow();
4055
}

grpc-contrib/src/test/java/com/salesforce/grpc/contrib/ServersTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public void shutdownGracefullyThrowsIfMaxWaitTimeInMillisIsZero() {
3434

3535
assertThatThrownBy(() -> Servers.shutdownGracefully(server, 0))
3636
.isInstanceOf(IllegalArgumentException.class)
37-
.hasMessageContaining("maxWaitTimeInMillis");
37+
.hasMessageContaining("timeout must be greater than 0");
3838
}
3939

4040
@Test
@@ -44,7 +44,7 @@ public void shutdownGracefullyThrowsIfMaxWaitTimeInMillisIsLessThanZero() {
4444

4545
assertThatThrownBy(() -> Servers.shutdownGracefully(server, maxWaitTimeInMillis))
4646
.isInstanceOf(IllegalArgumentException.class)
47-
.hasMessageContaining("maxWaitTimeInMillis");
47+
.hasMessageContaining("timeout must be greater than 0");
4848
}
4949

5050
@Test

grpc-testing-contrib/pom.xml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright (c) 2018, salesforce.com, inc.
4+
~ All rights reserved.
5+
~ Licensed under the BSD 3-Clause license.
6+
~ For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7+
-->
8+
9+
<project xmlns="http://maven.apache.org/POM/4.0.0"
10+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
11+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
12+
<parent>
13+
<artifactId>grpc-contrib-parent</artifactId>
14+
<groupId>com.salesforce.servicelibs</groupId>
15+
<version>0.7.1-SNAPSHOT</version>
16+
</parent>
17+
<modelVersion>4.0.0</modelVersion>
18+
19+
<artifactId>grpc-testing-contrib</artifactId>
20+
21+
<dependencies>
22+
<dependency>
23+
<groupId>io.grpc</groupId>
24+
<artifactId>grpc-core</artifactId>
25+
</dependency>
26+
<dependency>
27+
<groupId>io.grpc</groupId>
28+
<artifactId>grpc-stub</artifactId>
29+
</dependency>
30+
<dependency>
31+
<groupId>io.grpc</groupId>
32+
<artifactId>grpc-netty</artifactId>
33+
</dependency>
34+
<dependency>
35+
<groupId>com.salesforce.servicelibs</groupId>
36+
<artifactId>grpc-contrib</artifactId>
37+
</dependency>
38+
<dependency>
39+
<groupId>junit</groupId>
40+
<artifactId>junit</artifactId>
41+
<scope>compile</scope>
42+
</dependency>
43+
<dependency>
44+
<groupId>org.assertj</groupId>
45+
<artifactId>assertj-core</artifactId>
46+
<scope>test</scope>
47+
</dependency>
48+
<dependency>
49+
<groupId>io.grpc</groupId>
50+
<artifactId>grpc-testing-proto</artifactId>
51+
<scope>test</scope>
52+
</dependency>
53+
</dependencies>
54+
</project>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) 2017, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
package com.salesforce.grpc.testing.contrib;
9+
10+
import io.grpc.Context;
11+
import org.junit.Assert;
12+
import org.junit.rules.TestRule;
13+
import org.junit.runner.Description;
14+
import org.junit.runners.model.Statement;
15+
16+
/**
17+
* {@code GrpcContextRule} is a JUnit {@link TestRule} that forcibly resets the gRPC
18+
* {@link Context} to {@link Context#ROOT} between every unit test.
19+
*
20+
* <p>This rule makes it easier to correctly implement correct unit tests by preventing the
21+
* accidental leakage of context state between tests.
22+
*/
23+
public class GrpcContextRule implements TestRule {
24+
@Override
25+
public Statement apply(final Statement base, final Description description) {
26+
return new Statement() {
27+
@Override
28+
public void evaluate() throws Throwable {
29+
// Reset the gRPC context between test executions
30+
Context prev = Context.ROOT.attach();
31+
try {
32+
base.evaluate();
33+
if (Context.current() != Context.ROOT) {
34+
Assert.fail("Test is leaking context state between tests! Ensure proper " +
35+
"attach()/detach() pairing.");
36+
}
37+
} finally {
38+
Context.ROOT.detach(prev);
39+
}
40+
}
41+
};
42+
}
43+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright (c) 2017, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
package com.salesforce.grpc.testing.contrib;
9+
10+
import com.salesforce.grpc.contrib.Servers;
11+
import io.grpc.ManagedChannel;
12+
import io.grpc.Server;
13+
import io.grpc.netty.NettyChannelBuilder;
14+
import io.grpc.netty.NettyServerBuilder;
15+
import io.grpc.util.MutableHandlerRegistry;
16+
import org.junit.rules.ExternalResource;
17+
18+
import java.util.concurrent.TimeUnit;
19+
import java.util.function.Consumer;
20+
21+
import static com.google.common.base.Preconditions.checkNotNull;
22+
import static com.google.common.base.Preconditions.checkState;
23+
24+
/**
25+
* {@code NettyGrpcServerRule} is a JUnit {@link org.junit.rules.TestRule} that starts a gRPC Netty service with
26+
* a {@link MutableHandlerRegistry} for adding services. It is particularly useful for testing middleware and
27+
* interceptors using the "real" gRPC wire protocol instead of the InProcess protocol. While InProcess testing works
28+
* 99% of the time, the Netty and InProcess transports have different flow control and serialization semantics that
29+
* can have an affect on low-level gRPC integrations.
30+
*
31+
* <p>An {@link io.grpc.stub.AbstractStub} can be created against this service by using the
32+
* {@link ManagedChannel} provided by {@link NettyGrpcServerRule#getChannel()}.
33+
*/
34+
public class NettyGrpcServerRule extends ExternalResource {
35+
36+
private ManagedChannel channel;
37+
private Server server;
38+
private MutableHandlerRegistry serviceRegistry;
39+
private boolean useDirectExecutor;
40+
private int port = 0;
41+
42+
private Consumer<NettyServerBuilder> configureServerBuilder = sb -> { };
43+
private Consumer<NettyChannelBuilder> configureChannelBuilder = cb -> { };
44+
45+
/**
46+
* Provides a way to configure the {@code NettyServerBuilder} used for testing.
47+
*/
48+
public final NettyGrpcServerRule configureServerBuilder(Consumer<NettyServerBuilder> configureServerBuilder) {
49+
checkState(port == 0, "configureServerBuilder() can only be called at the rule instantiation");
50+
this.configureServerBuilder = checkNotNull(configureServerBuilder, "configureServerBuilder");
51+
return this;
52+
}
53+
54+
/**
55+
* Provides a way to configure the {@code NettyChannelBuilder} used for testing.
56+
*/
57+
public final NettyGrpcServerRule configureChannelBuilder(Consumer<NettyChannelBuilder> configureChannelBuilder) {
58+
checkState(port == 0, "configureChannelBuilder() can only be called at the rule instantiation");
59+
this.configureChannelBuilder = checkNotNull(configureChannelBuilder, "configureChannelBuilder");
60+
return this;
61+
}
62+
63+
/**
64+
* Returns a {@link ManagedChannel} connected to this service.
65+
*/
66+
public final ManagedChannel getChannel() {
67+
return channel;
68+
}
69+
70+
/**
71+
* Returns the underlying gRPC {@link Server} for this service.
72+
*/
73+
public final Server getServer() {
74+
return server;
75+
}
76+
77+
/**
78+
* Returns the randomly generated TCP port for this service.
79+
*/
80+
public final int getPort() {
81+
return port;
82+
}
83+
84+
/**
85+
* Returns the service registry for this service. The registry is used to add service instances
86+
* (e.g. {@link io.grpc.BindableService} or {@link io.grpc.ServerServiceDefinition} to the server.
87+
*/
88+
public final MutableHandlerRegistry getServiceRegistry() {
89+
return serviceRegistry;
90+
}
91+
92+
/**
93+
* Before the test has started, create the server and channel.
94+
*/
95+
@Override
96+
protected void before() throws Throwable {
97+
serviceRegistry = new MutableHandlerRegistry();
98+
99+
NettyServerBuilder serverBuilder = NettyServerBuilder
100+
.forPort(0)
101+
.fallbackHandlerRegistry(serviceRegistry);
102+
103+
if (useDirectExecutor) {
104+
serverBuilder.directExecutor();
105+
}
106+
107+
configureServerBuilder.accept(serverBuilder);
108+
server = serverBuilder.build().start();
109+
port = server.getPort();
110+
111+
NettyChannelBuilder channelBuilder = NettyChannelBuilder.forAddress("localhost", port).usePlaintext(true);
112+
configureChannelBuilder.accept(channelBuilder);
113+
channel = channelBuilder.build();
114+
}
115+
116+
/**
117+
* After the test has completed, clean up the channel and server.
118+
*/
119+
@Override
120+
protected void after() {
121+
serviceRegistry = null;
122+
123+
channel.shutdown();
124+
channel = null;
125+
port = 0;
126+
127+
try {
128+
Servers.shutdownGracefully(server, 1, TimeUnit.MINUTES);
129+
} catch (InterruptedException e) {
130+
Thread.currentThread().interrupt();
131+
throw new RuntimeException(e);
132+
} finally {
133+
server = null;
134+
}
135+
}
136+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright (c) 2018, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
package com.salesforce.grpc.testing.contrib;
9+
10+
import io.grpc.Context;
11+
import org.junit.Test;
12+
import org.junit.runner.Description;
13+
import org.junit.runners.model.Statement;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
17+
import static org.junit.Assert.fail;
18+
19+
public class GrpcContextRuleTest {
20+
@Test
21+
public void ruleSetsContextToRoot() {
22+
Context.current().withValue(Context.key("foo"), "bar").run(() -> {
23+
assertThat(Context.current()).isNotEqualTo(Context.ROOT);
24+
25+
try {
26+
GrpcContextRule rule = new GrpcContextRule();
27+
rule.apply(new Statement() {
28+
@Override
29+
public void evaluate() {
30+
assertThat(Context.current()).isEqualTo(Context.ROOT);
31+
}
32+
}, Description.createTestDescription(GrpcContextRuleTest.class, "ruleSetsContextToRoot"))
33+
.evaluate();
34+
} catch (Throwable throwable) {
35+
fail(throwable.getMessage());
36+
}
37+
});
38+
}
39+
40+
@Test
41+
public void ruleFailsIfContextLeaks() {
42+
Context.current().withValue(Context.key("foo"), "bar").run(() -> {
43+
assertThat(Context.current()).isNotEqualTo(Context.ROOT);
44+
45+
assertThatThrownBy(() -> {
46+
GrpcContextRule rule = new GrpcContextRule();
47+
rule.apply(new Statement() {
48+
@Override
49+
public void evaluate() {
50+
// Leak context
51+
Context.current().withValue(Context.key("cheese"), "baz").attach();
52+
}
53+
}, Description.createTestDescription(GrpcContextRuleTest.class, "ruleSetsContextToRoot"))
54+
.evaluate();
55+
}).isInstanceOf(AssertionError.class).hasMessageContaining("Test is leaking context");
56+
});
57+
}
58+
}

0 commit comments

Comments
 (0)