|
34 | 34 |
|
35 | 35 | import java.io.BufferedReader; |
36 | 36 | import java.io.BufferedWriter; |
| 37 | +import java.io.File; |
| 38 | +import java.io.FileWriter; |
37 | 39 | import java.io.IOException; |
38 | 40 | import java.io.InputStreamReader; |
39 | 41 | import java.io.OutputStreamWriter; |
|
52 | 54 | public class SharedMemoryTest { |
53 | 55 |
|
54 | 56 | @Test |
55 | | - public void testShmCreate() throws IOException { |
| 57 | + public void testShmCreate() throws Exception { |
56 | 58 | int rsize = 456; |
57 | 59 | try (SharedMemory shm = SharedMemory.create(rsize)) { |
58 | 60 | assertNotNull(shm.name()); |
@@ -89,72 +91,118 @@ public void testShmCreate() throws IOException { |
89 | 91 | } |
90 | 92 |
|
91 | 93 | @Test |
92 | | - public void testShmAttach() throws IOException { |
| 94 | + public void testShmAttach() throws Exception { |
93 | 95 | // 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" + |
100 | 100 | "from appose import SharedMemory\n" + |
101 | | - "from sys import stdout\n" + |
102 | 101 | "shm = SharedMemory(create=True, rsize=345)\n" + |
103 | 102 | "shm.buf[0] = 12\n" + |
104 | 103 | "shm.buf[100] = 234\n" + |
105 | 104 | "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" + |
109 | 108 | "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 | + } |
140 | 139 | } |
| 140 | + } |
| 141 | + |
| 142 | + private static String pythonCommand() { |
| 143 | + return Platforms.isWindows() ? "python.exe" : "python"; |
| 144 | + } |
141 | 145 |
|
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 | + } |
145 | 154 | } |
146 | 155 |
|
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); |
154 | 167 | } |
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(); |
157 | 173 | 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 | + } |
159 | 207 | } |
160 | 208 | } |
0 commit comments