Skip to content

Commit c38be2c

Browse files
ctruedenclaude
andcommitted
Use deterministic handshake in SharedMemoryTest
This hopefully fixes the flakiness on Windows CI (no more sleep(0.5)). * Changed Python script to use sys.stdin.read() to block until stdin closes * Added -u flag to Python command for unbuffered stdout * Created startPythonAndWait() helper that keeps stdin open * Java closes stdin in finally block to signal Python to cleanup * This ensures Python stays alive until Java completes its assertions Co-authored-by: Claude <noreply@anthropic.com>
1 parent 24703d8 commit c38be2c

File tree

1 file changed

+103
-55
lines changed

1 file changed

+103
-55
lines changed

src/test/java/org/apposed/appose/SharedMemoryTest.java

Lines changed: 103 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434

3535
import java.io.BufferedReader;
3636
import java.io.BufferedWriter;
37+
import java.io.File;
38+
import java.io.FileWriter;
3739
import java.io.IOException;
3840
import java.io.InputStreamReader;
3941
import java.io.OutputStreamWriter;
@@ -52,7 +54,7 @@
5254
public class SharedMemoryTest {
5355

5456
@Test
55-
public void testShmCreate() throws IOException {
57+
public void testShmCreate() throws Exception {
5658
int rsize = 456;
5759
try (SharedMemory shm = SharedMemory.create(rsize)) {
5860
assertNotNull(shm.name());
@@ -89,72 +91,118 @@ public void testShmCreate() throws IOException {
8991
}
9092

9193
@Test
92-
public void testShmAttach() throws IOException {
94+
public void testShmAttach() throws Exception {
9395
// Create a named shared memory block in a separate process.
94-
// NB: I originally tried passing the Python script as an argument to `-c`,
95-
// but it gets truncated (don't know why) and the program fails to execute.
96-
// So instead, the program is passed in via stdin. But that means the
97-
// program itself cannot read from stdin as a means of waiting for Java to
98-
// signal its completion of the test asserts; we use a hacky sleep instead.
99-
String output = runPython(
96+
// The Python process waits for a signal from stdin before unlinking,
97+
// ensuring deterministic coordination with the Java test.
98+
try (PythonProcess py = startPythonAndWait(
99+
"import sys\n" +
100100
"from appose import SharedMemory\n" +
101-
"from sys import stdout\n" +
102101
"shm = SharedMemory(create=True, rsize=345)\n" +
103102
"shm.buf[0] = 12\n" +
104103
"shm.buf[100] = 234\n" +
105104
"shm.buf[344] = 7\n" +
106-
"stdout.write(f'{shm.name}|{shm.rsize}|{shm.size}\\n')\n" +
107-
"stdout.flush()\n" +
108-
"import time; time.sleep(0.5)\n" + // HACK: horrible, but keeps things simple
105+
"sys.stdout.write(f'{shm.name}|{shm.rsize}|{shm.size}\\n')\n" +
106+
"sys.stdout.flush()\n" +
107+
"input() # Wait for Java to signal completion\n" +
109108
"shm.unlink()\n"
110-
);
111-
112-
// Parse the output into the name and size of the shared memory block.
113-
String[] shmInfo = output.split("\\|");
114-
assertEquals(3, shmInfo.length);
115-
String shmName = shmInfo[0];
116-
assertNotNull(shmName);
117-
assertFalse(shmName.isEmpty());
118-
int shmRSize = Integer.parseInt(shmInfo[1]);
119-
assertEquals(345, shmRSize);
120-
int shmSize = Integer.parseInt(shmInfo[2]);
121-
assertTrue(shmSize >= 345);
122-
123-
// Attach to the shared memory and verify it matches expectations.
124-
try (SharedMemory shm = SharedMemory.attach(shmName, shmRSize)) {
125-
assertNotNull(shm);
126-
assertEquals(shmName, shm.name());
127-
assertEquals(shmRSize, shm.rsize());
128-
// Note: We do not test that shmSize and shm.size() match exactly,
129-
// because Python and appose-java's SharedMemory code will not
130-
// necessarily behave identically when it comes to block rounding.
131-
// Notably, on Windows, Python does not round, whereas ShmWindows
132-
// rounds up to the next block size (4K on GitHub Actions CI).
133-
assertTrue(shm.size() >= 345);
134-
ByteBuffer buf = shm.buf();
135-
assertNotNull(buf);
136-
assertEquals(shmRSize, buf.limit());
137-
assertEquals(12, buf.get(0));
138-
assertEquals((byte) 234, buf.get(100));
139-
assertEquals(7, buf.get(344));
109+
)) {
110+
// Parse the output into the name and size of the shared memory block.
111+
String[] shmInfo = py.firstLine.split("\\|");
112+
assertEquals(3, shmInfo.length);
113+
String shmName = shmInfo[0];
114+
assertNotNull(shmName);
115+
assertFalse(shmName.isEmpty());
116+
int shmRSize = Integer.parseInt(shmInfo[1]);
117+
assertEquals(345, shmRSize);
118+
int shmSize = Integer.parseInt(shmInfo[2]);
119+
assertTrue(shmSize >= 345);
120+
121+
// Attach to the shared memory and verify it matches expectations.
122+
try (SharedMemory shm = SharedMemory.attach(shmName, shmRSize)) {
123+
assertNotNull(shm);
124+
assertEquals(shmName, shm.name());
125+
assertEquals(shmRSize, shm.rsize());
126+
// Note: We do not test that shmSize and shm.size() match exactly,
127+
// because Python and appose-java's SharedMemory code will not
128+
// necessarily behave identically when it comes to block rounding.
129+
// Notably, on Windows, Python does not round, whereas ShmWindows
130+
// rounds up to the next block size (4K on GitHub Actions CI).
131+
assertTrue(shm.size() >= 345);
132+
ByteBuffer buf = shm.buf();
133+
assertNotNull(buf);
134+
assertEquals(shmRSize, buf.limit());
135+
assertEquals(12, buf.get(0));
136+
assertEquals((byte) 234, buf.get(100));
137+
assertEquals(7, buf.get(344));
138+
}
140139
}
140+
}
141+
142+
private static String pythonCommand() {
143+
return Platforms.isWindows() ? "python.exe" : "python";
144+
}
141145

142-
// NB: No need to clean up the shared memory explicitly,
143-
// since the Python program will unlink it and terminate
144-
// upon completion of the sleep instruction.
146+
/**
147+
* Runs a Python script, reads the first line of output, and waits for completion.
148+
* For scripts that need to coordinate with Java before exiting, use startPythonAndWait.
149+
*/
150+
private static String runPython(String script) throws Exception {
151+
try (PythonProcess py = startPythonAndWait(script)) {
152+
return py.firstLine;
153+
}
145154
}
146155

147-
private static String runPython(String script) throws IOException {
148-
String pythonCommand = Platforms.isWindows() ? "python.exe" : "python";
149-
ProcessBuilder pb = new ProcessBuilder().command(pythonCommand);
150-
Process p = pb.start();
151-
try (BufferedWriter os = new BufferedWriter(new OutputStreamWriter(p.getOutputStream()))) {
152-
os.write(script);
153-
os.flush();
156+
/**
157+
* Starts a Python process with the given script, waits for first line of output,
158+
* but keeps the process alive for later coordination via stdin.
159+
* The returned PythonProcess must be closed (signals Python to exit via stdin).
160+
*/
161+
private static PythonProcess startPythonAndWait(String script) throws IOException {
162+
// Write script to a temp file so stdin is available for coordination
163+
File tempScript = File.createTempFile("appose-test-", ".py");
164+
tempScript.deleteOnExit();
165+
try (FileWriter fw = new FileWriter(tempScript)) {
166+
fw.write(script);
154167
}
155-
BufferedReader is = new BufferedReader(new InputStreamReader(p.getInputStream()));
156-
String line = is.readLine();
168+
169+
Process p = new ProcessBuilder(pythonCommand(), "-u", tempScript.getAbsolutePath()).start();
170+
BufferedWriter stdin = new BufferedWriter(new OutputStreamWriter(p.getOutputStream()));
171+
BufferedReader stdout = new BufferedReader(new InputStreamReader(p.getInputStream()));
172+
String line = stdout.readLine();
157173
assertNotNull(line, "Python program returned no output");
158-
return line;
174+
return new PythonProcess(p, stdin, line);
175+
}
176+
177+
/**
178+
* Helper class to hold a running Python process and its first output line.
179+
* Implements AutoCloseable to handle cleanup automatically.
180+
*/
181+
private static class PythonProcess implements AutoCloseable {
182+
final Process process;
183+
final BufferedWriter stdin;
184+
final String firstLine;
185+
186+
PythonProcess(Process process, BufferedWriter stdin, String firstLine) {
187+
this.process = process;
188+
this.stdin = stdin;
189+
this.firstLine = firstLine;
190+
}
191+
192+
@Override
193+
public void close() throws Exception {
194+
// Signal Python to exit by sending a newline and closing stdin
195+
try {
196+
stdin.write("\n");
197+
stdin.flush();
198+
stdin.close();
199+
} catch (IOException e) {
200+
// Ignore - process may have already exited
201+
}
202+
boolean exited = process.waitFor(2, java.util.concurrent.TimeUnit.SECONDS);
203+
if (!exited) {
204+
process.destroyForcibly();
205+
}
206+
}
159207
}
160208
}

0 commit comments

Comments
 (0)