Skip to content

Commit 4567c49

Browse files
pongadgarrettjonesgoogle
authored andcommitted
pubsub: fix testStartStopReset (#1453)
The test verifies that calling stop() actually stops the emulator. The previous implementation stops the emulator by - sending HTTP POST to /shutdown - terminating the emulator process The second step follows directly after the first. It then queries the server it just shut down to make sure it does not respond. This behavior is incorrect for two reasons. - The server might continue serving request after responding to /shutdown. - The "emulator process" might not be the emulator at all. It might be a script that in-turn runs the emulator. On UNIX systems, killing a parent process does not kill any of its children; the actual emulator might run indefinitely after we kill the process we spawned. This commit changes this behavior. After POSTing to /shutdown, we simply wait for the spawned process to terminate on its own. Technically, there is still a race: Another emulator could have been started on the same port. When we send a request to verify that the old emulator has terminated, we might get a response from the new one. Since we execute tests sequentially though, I believe this should not be a problem in practice.
1 parent cacf65e commit 4567c49

File tree

9 files changed

+91
-40
lines changed

9 files changed

+91
-40
lines changed

google-cloud-core/src/main/java/com/google/cloud/testing/BaseEmulatorHelper.java

+51-16
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import com.google.cloud.ServiceOptions;
2020
import com.google.common.io.CharStreams;
21+
import com.google.common.util.concurrent.SettableFuture;
22+
import com.google.common.util.concurrent.UncheckedExecutionException;
2123

2224
import java.io.BufferedInputStream;
2325
import java.io.BufferedOutputStream;
@@ -43,10 +45,14 @@
4345
import java.util.List;
4446
import java.util.Locale;
4547
import java.util.Map;
48+
import java.util.concurrent.ExecutionException;
49+
import java.util.concurrent.TimeUnit;
50+
import java.util.concurrent.TimeoutException;
4651
import java.util.logging.Level;
4752
import java.util.logging.Logger;
4853
import java.util.zip.ZipEntry;
4954
import java.util.zip.ZipInputStream;
55+
import org.joda.time.Duration;
5056

5157
/**
5258
* Utility class to start and stop a local service which is used by unit testing.
@@ -104,16 +110,50 @@ protected final void startProcess(String blockUntilOutput)
104110
}
105111

106112
/**
107-
* Stops the local service's subprocess and any possible thread listening for its output.
113+
* Waits for the local service's subprocess to terminate,
114+
* and stop any possible thread listening for its output.
108115
*/
109-
protected final void stopProcess() throws IOException, InterruptedException {
116+
protected final int waitForProcess(Duration timeout) throws IOException, InterruptedException, TimeoutException {
110117
if (blockingProcessReader != null) {
111118
blockingProcessReader.terminate();
112119
blockingProcessReader = null;
113120
}
114121
if (activeRunner != null) {
115-
activeRunner.stop();
122+
int exitCode = activeRunner.waitFor(timeout);
116123
activeRunner = null;
124+
return exitCode;
125+
}
126+
return 0;
127+
}
128+
129+
private static int waitForProcess(final Process process, Duration timeout) throws InterruptedException, TimeoutException {
130+
if (process == null) {
131+
return 0;
132+
}
133+
134+
final SettableFuture<Integer> exitValue = SettableFuture.create();
135+
136+
Thread waiter = new Thread(new Runnable() {
137+
@Override
138+
public void run() {
139+
try {
140+
exitValue.set(process.waitFor());
141+
} catch (InterruptedException e) {
142+
exitValue.setException(e);
143+
}
144+
}
145+
});
146+
waiter.start();
147+
148+
try {
149+
return exitValue.get(timeout.getMillis(), TimeUnit.MILLISECONDS);
150+
} catch (ExecutionException e) {
151+
if (e.getCause() instanceof InterruptedException) {
152+
throw (InterruptedException) e.getCause();
153+
}
154+
throw new UncheckedExecutionException(e);
155+
} finally {
156+
waiter.interrupt();
117157
}
118158
}
119159

@@ -144,7 +184,7 @@ public String getProjectId() {
144184
/**
145185
* Stops the local emulator.
146186
*/
147-
public abstract void stop() throws IOException, InterruptedException;
187+
public abstract void stop(Duration timeout) throws IOException, InterruptedException, TimeoutException;
148188

149189
/**
150190
* Resets the internal state of the emulator.
@@ -195,9 +235,10 @@ protected interface EmulatorRunner {
195235
void start() throws IOException;
196236

197237
/**
198-
* Stops the emulator associated to this runner.
238+
* Wait for the emulator associated to this runner to terminate,
239+
* returning the exit status.
199240
*/
200-
void stop() throws InterruptedException;
241+
int waitFor(Duration timeout) throws InterruptedException, TimeoutException;
201242

202243
/**
203244
* Returns the process associated to the emulator, if any.
@@ -239,11 +280,8 @@ public void start() throws IOException {
239280
}
240281

241282
@Override
242-
public void stop() throws InterruptedException {
243-
if (process != null) {
244-
process.destroy();
245-
process.waitFor();
246-
}
283+
public int waitFor(Duration timeout) throws InterruptedException, TimeoutException {
284+
return waitForProcess(process, timeout);
247285
}
248286

249287
@Override
@@ -337,11 +375,8 @@ public void start() throws IOException {
337375
}
338376

339377
@Override
340-
public void stop() throws InterruptedException {
341-
if (process != null) {
342-
process.destroy();
343-
process.waitFor();
344-
}
378+
public int waitFor(Duration timeout) throws InterruptedException, TimeoutException {
379+
return waitForProcess(process, timeout);
345380
}
346381

347382
@Override

google-cloud-core/src/test/java/com/google/cloud/testing/BaseEmulatorHelperTest.java

+12-10
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
import com.google.common.collect.ImmutableList;
2323

2424
import org.easymock.EasyMock;
25+
import org.joda.time.Duration;
2526
import org.junit.Test;
2627

2728
import java.io.ByteArrayInputStream;
2829
import java.io.IOException;
2930
import java.io.InputStream;
3031
import java.util.List;
32+
import java.util.concurrent.TimeoutException;
3133
import java.util.logging.Logger;
3234

3335
public class BaseEmulatorHelperTest {
@@ -66,8 +68,8 @@ public void start() throws IOException, InterruptedException {
6668
}
6769

6870
@Override
69-
public void stop() throws IOException, InterruptedException {
70-
stopProcess();
71+
public void stop(Duration timeout) throws IOException, InterruptedException, TimeoutException {
72+
waitForProcess(timeout);
7173
}
7274

7375
@Override
@@ -77,7 +79,7 @@ public void reset() throws IOException {
7779
}
7880

7981
@Test
80-
public void testEmulatorHelper() throws IOException, InterruptedException {
82+
public void testEmulatorHelper() throws IOException, InterruptedException, TimeoutException {
8183
Process process = EasyMock.createStrictMock(Process.class);
8284
InputStream stream = new ByteArrayInputStream(BLOCK_UNTIL.getBytes(Charsets.UTF_8));
8385
EmulatorRunner emulatorRunner = EasyMock.createStrictMock(EmulatorRunner.class);
@@ -86,18 +88,18 @@ public void testEmulatorHelper() throws IOException, InterruptedException {
8688
emulatorRunner.start();
8789
EasyMock.expectLastCall();
8890
EasyMock.expect(emulatorRunner.getProcess()).andReturn(process);
89-
emulatorRunner.stop();
90-
EasyMock.expectLastCall();
91+
emulatorRunner.waitFor(Duration.standardMinutes(1));
92+
EasyMock.expectLastCall().andReturn(0);
9193
EasyMock.replay(process, emulatorRunner);
9294
TestEmulatorHelper helper =
9395
new TestEmulatorHelper(ImmutableList.of(emulatorRunner), BLOCK_UNTIL);
9496
helper.start();
95-
helper.stop();
97+
helper.stop(Duration.standardMinutes(1));
9698
EasyMock.verify();
9799
}
98100

99101
@Test
100-
public void testEmulatorHelperMultipleRunners() throws IOException, InterruptedException {
102+
public void testEmulatorHelperMultipleRunners() throws IOException, InterruptedException, TimeoutException {
101103
Process process = EasyMock.createStrictMock(Process.class);
102104
InputStream stream = new ByteArrayInputStream(BLOCK_UNTIL.getBytes(Charsets.UTF_8));
103105
EmulatorRunner firstRunner = EasyMock.createStrictMock(EmulatorRunner.class);
@@ -108,13 +110,13 @@ public void testEmulatorHelperMultipleRunners() throws IOException, InterruptedE
108110
secondRunner.start();
109111
EasyMock.expectLastCall();
110112
EasyMock.expect(secondRunner.getProcess()).andReturn(process);
111-
secondRunner.stop();
112-
EasyMock.expectLastCall();
113+
secondRunner.waitFor(Duration.standardMinutes(1));
114+
EasyMock.expectLastCall().andReturn(0);
113115
EasyMock.replay(process, secondRunner);
114116
TestEmulatorHelper helper =
115117
new TestEmulatorHelper(ImmutableList.of(firstRunner, secondRunner), BLOCK_UNTIL);
116118
helper.start();
117-
helper.stop();
119+
helper.stop(Duration.standardMinutes(1));
118120
EasyMock.verify();
119121
}
120122
}

google-cloud-datastore/src/main/java/com/google/cloud/datastore/testing/LocalDatastoreHelper.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import com.google.cloud.testing.BaseEmulatorHelper;
2323
import com.google.common.collect.ImmutableList;
2424

25+
import org.joda.time.Duration;
26+
2527
import java.io.IOException;
2628
import java.net.MalformedURLException;
2729
import java.net.URL;
@@ -34,6 +36,7 @@
3436
import java.util.Arrays;
3537
import java.util.List;
3638
import java.util.UUID;
39+
import java.util.concurrent.TimeoutException;
3740
import java.util.logging.Level;
3841
import java.util.logging.Logger;
3942

@@ -225,9 +228,9 @@ public void reset() throws IOException {
225228
/**
226229
* Stops the Datastore emulator.
227230
*/
228-
public void stop() throws IOException, InterruptedException {
231+
public void stop(Duration timeout) throws IOException, InterruptedException, TimeoutException {
229232
sendPostRequest("/shutdown");
230-
stopProcess();
233+
waitForProcess(timeout);
231234
deleteRecursively(gcdPath);
232235
}
233236

google-cloud-datastore/src/test/java/com/google/cloud/datastore/DatastoreTest.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,15 @@
4747
import com.google.protobuf.ByteString;
4848

4949
import org.easymock.EasyMock;
50+
import org.joda.time.Duration;
5051
import org.junit.AfterClass;
5152
import org.junit.Before;
5253
import org.junit.BeforeClass;
5354
import org.junit.Rule;
54-
import org.junit.Test;
5555
import org.junit.rules.ExpectedException;
5656
import org.junit.runner.RunWith;
5757
import org.junit.runners.JUnit4;
58+
import org.junit.Test;
5859

5960
import java.io.IOException;
6061
import java.util.ArrayList;
@@ -63,6 +64,7 @@
6364
import java.util.Iterator;
6465
import java.util.List;
6566
import java.util.Set;
67+
import java.util.concurrent.TimeoutException;
6668

6769
@RunWith(JUnit4.class)
6870
public class DatastoreTest {
@@ -150,8 +152,8 @@ public void setUp() {
150152
}
151153

152154
@AfterClass
153-
public static void afterClass() throws IOException, InterruptedException {
154-
helper.stop();
155+
public static void afterClass() throws IOException, InterruptedException, TimeoutException {
156+
helper.stop(Duration.standardMinutes(1));
155157
}
156158

157159
@Test

google-cloud-datastore/src/test/java/com/google/cloud/datastore/testing/LocalDatastoreHelperTest.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@
2929
import com.google.cloud.datastore.Entity;
3030
import com.google.cloud.datastore.Key;
3131

32+
import org.joda.time.Duration;
3233
import org.junit.Rule;
3334
import org.junit.Test;
3435
import org.junit.rules.ExpectedException;
3536
import org.junit.runner.RunWith;
3637
import org.junit.runners.JUnit4;
3738

3839
import java.io.IOException;
40+
import java.util.concurrent.TimeoutException;
3941

4042
@RunWith(JUnit4.class)
4143
public class LocalDatastoreHelperTest {
@@ -82,7 +84,7 @@ public void testOptions() {
8284
}
8385

8486
@Test
85-
public void testStartStopReset() throws IOException, InterruptedException {
87+
public void testStartStopReset() throws IOException, InterruptedException, TimeoutException {
8688
LocalDatastoreHelper helper = LocalDatastoreHelper.create();
8789
helper.start();
8890
Datastore datastore = helper.getOptions().getService();
@@ -91,7 +93,7 @@ public void testStartStopReset() throws IOException, InterruptedException {
9193
assertNotNull(datastore.get(key));
9294
helper.reset();
9395
assertNull(datastore.get(key));
94-
helper.stop();
96+
helper.stop(Duration.standardMinutes(1));
9597
thrown.expect(DatastoreException.class);
9698
datastore.get(key);
9799
}

google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/testing/LocalPubSubHelper.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@
3333
import java.util.Arrays;
3434
import java.util.List;
3535
import java.util.UUID;
36+
import java.util.concurrent.TimeoutException;
3637
import java.util.logging.Logger;
3738

39+
import org.joda.time.Duration;
40+
3841
/**
3942
* A class that runs a Pubsub emulator instance for use in tests.
4043
*/
@@ -144,8 +147,8 @@ public void reset() throws IOException {
144147
* Stops the PubSub emulator and related local service.
145148
*/
146149
@Override
147-
public void stop() throws IOException, InterruptedException {
150+
public void stop(Duration timeout) throws IOException, InterruptedException, TimeoutException {
148151
sendPostRequest("/shutdown");
149-
stopProcess();
152+
waitForProcess(timeout);
150153
}
151154
}

google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/LocalSystemTest.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.cloud.pubsub.testing.LocalPubSubHelper;
2020

21+
import org.joda.time.Duration;
2122
import org.junit.AfterClass;
2223
import org.junit.BeforeClass;
2324

@@ -49,6 +50,6 @@ public static void startServer() throws IOException, InterruptedException {
4950
public static void stopServer() throws Exception {
5051
pubsub.close();
5152
pubsubHelper.reset();
52-
pubsubHelper.stop();
53+
pubsubHelper.stop(Duration.standardMinutes(1));
5354
}
5455
}

google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/spi/v1/PublisherClientTest.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.ArrayList;
4141
import java.util.Collections;
4242
import java.util.List;
43+
import java.util.concurrent.TimeoutException;
4344

4445
public class PublisherClientTest {
4546
private static LocalPubSubHelper pubsubHelper;
@@ -55,8 +56,8 @@ public static void startServer() throws IOException, InterruptedException {
5556
}
5657

5758
@AfterClass
58-
public static void stopServer() throws IOException, InterruptedException {
59-
pubsubHelper.stop();
59+
public static void stopServer() throws IOException, InterruptedException, TimeoutException {
60+
pubsubHelper.stop(Duration.standardMinutes(1));
6061
}
6162

6263
@Before

google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/testing/LocalPubSubHelperTest.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
import com.google.cloud.pubsub.PubSubOptions;
2828
import com.google.cloud.pubsub.TopicInfo;
2929

30+
import org.joda.time.Duration;
3031
import org.junit.Rule;
3132
import org.junit.Test;
3233
import org.junit.rules.ExpectedException;
3334

3435
import java.io.IOException;
36+
import java.util.concurrent.TimeoutException;
3537

3638
public class LocalPubSubHelperTest {
3739

@@ -57,15 +59,15 @@ public void testOptions() {
5759
}
5860

5961
@Test
60-
public void testStartStopReset() throws IOException, InterruptedException {
62+
public void testStartStopReset() throws IOException, InterruptedException, TimeoutException {
6163
LocalPubSubHelper helper = LocalPubSubHelper.create();
6264
helper.start();
6365
PubSub pubsub = helper.getOptions().getService();
6466
pubsub.create(TopicInfo.of(TOPIC));
6567
assertNotNull(pubsub.getTopic(TOPIC));
6668
helper.reset();
6769
assertNull(pubsub.getTopic(TOPIC));
68-
helper.stop();
70+
helper.stop(Duration.standardMinutes(1));
6971
thrown.expect(PubSubException.class);
7072
pubsub.getTopic(TOPIC);
7173
}

0 commit comments

Comments
 (0)