Skip to content

Commit a9b2f64

Browse files
ctruedenclaude
andcommitted
Add NDArrayTest covering 2D–5D shapes via Python and Groovy workers
Replaces the ad-hoc NDArrayExamplePython and NDArrayExampleGroovy main methods with a proper JUnit 5 test class. Tests NDArray shape metadata round-trips from 2D through 5D for both C_ORDER and F_ORDER inputs, validating: - Java-side: ndim, dims, order, and shm size - Python worker (via uv+numpy, shared across tests with @BeforeAll): shape and contiguity flags from numpy - Groovy worker: shape and order from NDArray.Shape Documents and asserts the wire-format behavior: Messages.java always normalizes shape to C_ORDER during serialization, so workers always receive a C_ORDER array regardless of the original indexing convention. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ac5bb38 commit a9b2f64

File tree

3 files changed

+204
-148
lines changed

3 files changed

+204
-148
lines changed

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

Lines changed: 0 additions & 75 deletions
This file was deleted.

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

Lines changed: 0 additions & 73 deletions
This file was deleted.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*-
2+
* #%L
3+
* Appose: multi-language interprocess cooperation with shared memory.
4+
* %%
5+
* Copyright (C) 2023 - 2026 Appose developers.
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
* #L%
28+
*/
29+
30+
package org.apposed.appose;
31+
32+
import org.junit.jupiter.api.AfterAll;
33+
import org.junit.jupiter.api.BeforeAll;
34+
import org.junit.jupiter.api.Test;
35+
36+
import java.nio.FloatBuffer;
37+
import java.util.Arrays;
38+
import java.util.HashMap;
39+
import java.util.List;
40+
import java.util.Map;
41+
42+
import static org.apposed.appose.NDArray.Shape.Order.C_ORDER;
43+
import static org.apposed.appose.NDArray.Shape.Order.F_ORDER;
44+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
45+
import static org.junit.jupiter.api.Assertions.assertEquals;
46+
import static org.junit.jupiter.api.Assertions.assertTrue;
47+
48+
/**
49+
* Tests {@link NDArray} shapes from 2D through 5D, validating that the shape
50+
* is preserved correctly on both the Java (service) side and the Python /
51+
* Groovy (worker/subprocess) side.
52+
*/
53+
public class NDArrayTest extends TestBase {
54+
55+
private static Service python;
56+
57+
@BeforeAll
58+
public static void setUp() throws Exception {
59+
python = Appose.uv()
60+
.include("numpy")
61+
.base("target/envs/ndarray-check-python")
62+
.build()
63+
.python()
64+
.init("import numpy");
65+
}
66+
67+
@AfterAll
68+
public static void tearDown() {
69+
if (python != null && python.isAlive()) python.close();
70+
}
71+
72+
// 2-D through 5-D test cases, one method per (ndim, order) combination.
73+
74+
@Test public void testPython2dFOrder() throws Exception { checkPython(F_ORDER, 3, 5); }
75+
@Test public void testPython2dCOrder() throws Exception { checkPython(C_ORDER, 3, 5); }
76+
77+
@Test public void testPython3dFOrder() throws Exception { checkPython(F_ORDER, 4, 3, 2); }
78+
@Test public void testPython3dCOrder() throws Exception { checkPython(C_ORDER, 4, 3, 2); }
79+
80+
@Test public void testPython4dFOrder() throws Exception { checkPython(F_ORDER, 2, 3, 4, 5); }
81+
@Test public void testPython4dCOrder() throws Exception { checkPython(C_ORDER, 2, 3, 4, 5); }
82+
83+
@Test public void testPython5dFOrder() throws Exception { checkPython(F_ORDER, 2, 3, 2, 3, 2); }
84+
@Test public void testPython5dCOrder() throws Exception { checkPython(C_ORDER, 2, 3, 2, 3, 2); }
85+
86+
@Test public void testGroovy2dFOrder() throws Exception { checkGroovy(F_ORDER, 3, 5); }
87+
@Test public void testGroovy2dCOrder() throws Exception { checkGroovy(C_ORDER, 3, 5); }
88+
89+
@Test public void testGroovy3dFOrder() throws Exception { checkGroovy(F_ORDER, 4, 3, 2); }
90+
@Test public void testGroovy3dCOrder() throws Exception { checkGroovy(C_ORDER, 4, 3, 2); }
91+
92+
@Test public void testGroovy4dFOrder() throws Exception { checkGroovy(F_ORDER, 2, 3, 4, 5); }
93+
@Test public void testGroovy4dCOrder() throws Exception { checkGroovy(C_ORDER, 2, 3, 4, 5); }
94+
95+
@Test public void testGroovy5dFOrder() throws Exception { checkGroovy(F_ORDER, 2, 3, 2, 3, 2); }
96+
@Test public void testGroovy5dCOrder() throws Exception { checkGroovy(C_ORDER, 2, 3, 2, 3, 2); }
97+
98+
// -----------------------------------------------------------------------
99+
// Helpers
100+
// -----------------------------------------------------------------------
101+
102+
private void checkPython(NDArray.Shape.Order order, int... dims) throws Exception {
103+
NDArray.Shape shape = new NDArray.Shape(order, dims);
104+
try (NDArray ndArray = filledArray(shape)) {
105+
// Java-side validation
106+
assertShape(shape, ndArray);
107+
108+
// Python-side validation: the script returns the numpy shape and order.
109+
Map<String, Object> inputs = new HashMap<>();
110+
inputs.put("arr", ndArray);
111+
Service.Task task = python.task(PYTHON_SHAPE_SCRIPT, inputs);
112+
task.waitFor();
113+
assertComplete(task);
114+
115+
@SuppressWarnings("unchecked")
116+
List<Number> workerShape = (List<Number>) task.outputs.get("shape");
117+
// Wire format normalizes to C_ORDER, so numpy always receives a C_ORDER array.
118+
int[] expectedCShape = shape.toIntArray(C_ORDER);
119+
int[] actualCShape = workerShape.stream().mapToInt(Number::intValue).toArray();
120+
assertArrayEquals(expectedCShape, actualCShape,
121+
"Python shape mismatch for order=" + order + " dims=" + Arrays.toString(dims));
122+
123+
// Regardless of the original order, the worker always receives C_ORDER.
124+
assertEquals("C", task.outputs.get("order"),
125+
"Python order mismatch for order=" + order + " dims=" + Arrays.toString(dims));
126+
}
127+
}
128+
129+
private void checkGroovy(NDArray.Shape.Order order, int... dims) throws Exception {
130+
NDArray.Shape shape = new NDArray.Shape(order, dims);
131+
try (NDArray ndArray = filledArray(shape)) {
132+
// Java-side validation
133+
assertShape(shape, ndArray);
134+
135+
// Groovy-side validation: the script returns shape dims and order name.
136+
// Note: the Appose wire format always normalizes shape to C_ORDER
137+
// (see Messages.java), so the worker always receives a C_ORDER array.
138+
Environment env = Appose.system();
139+
try (Service service = env.groovy()) {
140+
maybeDebug(service);
141+
Map<String, Object> inputs = new HashMap<>();
142+
inputs.put("arr", ndArray);
143+
Service.Task task = service.task(GROOVY_SHAPE_SCRIPT, inputs);
144+
task.waitFor();
145+
assertComplete(task);
146+
147+
@SuppressWarnings("unchecked")
148+
List<Number> workerShape = (List<Number>) task.outputs.get("shape");
149+
// Wire format normalizes to C_ORDER, so worker always sees C_ORDER dims.
150+
int[] expectedCDims = shape.toIntArray(C_ORDER);
151+
int[] actualDims = workerShape.stream().mapToInt(Number::intValue).toArray();
152+
assertArrayEquals(expectedCDims, actualDims,
153+
"Groovy shape mismatch for order=" + order + " dims=" + Arrays.toString(dims));
154+
155+
assertEquals(C_ORDER.name(), task.outputs.get("order"),
156+
"Groovy order mismatch for order=" + order + " dims=" + Arrays.toString(dims));
157+
}
158+
}
159+
}
160+
161+
/** Creates a FLOAT32 NDArray with the given shape, filled 0, 1, 2, … */
162+
private static NDArray filledArray(NDArray.Shape shape) {
163+
NDArray ndArray = new NDArray(NDArray.DType.FLOAT32, shape);
164+
FloatBuffer buf = ndArray.buffer().asFloatBuffer();
165+
long len = shape.numElements();
166+
for (int i = 0; i < len; i++) buf.put(i, i);
167+
return ndArray;
168+
}
169+
170+
/** Validates Java-side shape metadata. */
171+
private static void assertShape(NDArray.Shape expected, NDArray ndArray) {
172+
assertEquals(expected.length(), ndArray.shape().length(),
173+
"Java ndim mismatch");
174+
assertArrayEquals(expected.toIntArray(), ndArray.shape().toIntArray(),
175+
"Java dims mismatch");
176+
assertEquals(expected.order(), ndArray.shape().order(),
177+
"Java order mismatch");
178+
long minBytes = expected.numElements() * NDArray.DType.FLOAT32.bytesPerElement();
179+
assertTrue(ndArray.shm().size() >= minBytes,
180+
"Shared memory too small: " + ndArray.shm().size() + " < " + minBytes);
181+
}
182+
183+
// -----------------------------------------------------------------------
184+
// Worker scripts
185+
// -----------------------------------------------------------------------
186+
187+
/**
188+
* Python script: receives {@code arr} (appose NDArray, accessible as a
189+
* numpy ndarray via {@code arr.ndarray()}), returns its shape and memory order.
190+
*/
191+
private static final String PYTHON_SHAPE_SCRIPT =
192+
"na = arr.ndarray()\n" +
193+
"task.outputs['shape'] = list(na.shape)\n" +
194+
"task.outputs['order'] = 'F' if na.flags['F_CONTIGUOUS'] and not na.flags['C_CONTIGUOUS'] else 'C'\n";
195+
196+
/**
197+
* Groovy script: receives {@code arr} (appose NDArray), returns its shape
198+
* dims (which always end up in C order) and order name.
199+
*/
200+
private static final String GROOVY_SHAPE_SCRIPT =
201+
"def s = arr.shape()\n" +
202+
"task.outputs['shape'] = s.toIntArray().toList()\n" +
203+
"task.outputs['order'] = s.order().name()\n";
204+
}

0 commit comments

Comments
 (0)