js4j 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +684 -0
- package/examples/01-basic.js +75 -0
- package/examples/02-collections.js +108 -0
- package/examples/03-objects.js +99 -0
- package/examples/04-callbacks.js +97 -0
- package/examples/05-stdlib.js +123 -0
- package/examples/_gateway.js +78 -0
- package/index.d.ts +455 -0
- package/index.js +78 -0
- package/java/TestEntryPoint.java +262 -0
- package/java/py4j.jar +0 -0
- package/package.json +29 -0
- package/src/callbackServer.js +269 -0
- package/src/clientServer.js +81 -0
- package/src/collections.js +325 -0
- package/src/connection.js +253 -0
- package/src/exceptions.js +68 -0
- package/src/gateway.js +222 -0
- package/src/javaObject.js +267 -0
- package/src/jvmView.js +224 -0
- package/src/launcher.js +150 -0
- package/src/protocol.js +413 -0
- package/tests/comparison/run_comparison.js +279 -0
- package/tests/comparison/test_js4j.js +187 -0
- package/tests/comparison/test_py4j.py +171 -0
- package/tests/integration/gateway.integration.test.js +384 -0
- package/tests/unit/collections.test.js +313 -0
- package/tests/unit/exceptions.test.js +104 -0
- package/tests/unit/gateway.test.js +178 -0
- package/tests/unit/jvmView.test.js +126 -0
- package/tests/unit/launcher.test.js +27 -0
- package/tests/unit/protocol.test.js +280 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration tests — require a live Java GatewayServer.
|
|
5
|
+
*
|
|
6
|
+
* These tests start the Java TestEntryPoint server as a child process,
|
|
7
|
+
* run the full js4j client against it, and verify correct behaviour.
|
|
8
|
+
*
|
|
9
|
+
* To run:
|
|
10
|
+
* PY4J_JAR=/path/to/py4j.jar npm run test:integration
|
|
11
|
+
*
|
|
12
|
+
* Or if py4j is installed via pip, the JAR path is auto-detected.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { spawn, execSync } = require('child_process');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const net = require('net');
|
|
19
|
+
|
|
20
|
+
const { JavaGateway, GatewayParameters, Js4JJavaError } = require('../../index');
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Locate py4j JAR
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
function findPy4jJar() {
|
|
27
|
+
if (process.env.PY4J_JAR) return process.env.PY4J_JAR;
|
|
28
|
+
try {
|
|
29
|
+
const site = execSync(
|
|
30
|
+
'python3 -c "import py4j, os; print(os.path.dirname(py4j.__file__))"',
|
|
31
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
32
|
+
).trim();
|
|
33
|
+
const libDir = path.join(site, 'java', 'lib');
|
|
34
|
+
if (fs.existsSync(libDir)) {
|
|
35
|
+
const jars = fs.readdirSync(libDir).filter(f => f.endsWith('.jar'));
|
|
36
|
+
if (jars.length > 0) return path.join(libDir, jars[0]);
|
|
37
|
+
}
|
|
38
|
+
} catch (_) {}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PY4J_JAR = findPy4jJar();
|
|
43
|
+
const ROOT = path.resolve(__dirname, '../..');
|
|
44
|
+
const BUILD_DIR = path.join(ROOT, 'java', 'build');
|
|
45
|
+
const JAVA_SRC = path.join(ROOT, 'java', 'TestEntryPoint.java');
|
|
46
|
+
|
|
47
|
+
const SKIP = !PY4J_JAR;
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Gateway server lifecycle
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
let gatewayProcess = null;
|
|
54
|
+
let gateway = null;
|
|
55
|
+
let gatewayPort = 25433; // use non-default port to avoid conflicts
|
|
56
|
+
|
|
57
|
+
function waitForPort(port, timeout = 10000) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const start = Date.now();
|
|
60
|
+
function attempt() {
|
|
61
|
+
const s = new net.Socket();
|
|
62
|
+
s.setTimeout(500);
|
|
63
|
+
s.on('connect', () => { s.destroy(); resolve(); });
|
|
64
|
+
s.on('error', () => {
|
|
65
|
+
s.destroy();
|
|
66
|
+
if (Date.now() - start > timeout) return reject(new Error(`Port ${port} not ready`));
|
|
67
|
+
setTimeout(attempt, 200);
|
|
68
|
+
});
|
|
69
|
+
s.on('timeout', () => { s.destroy(); setTimeout(attempt, 200); });
|
|
70
|
+
s.connect(port, '127.0.0.1');
|
|
71
|
+
}
|
|
72
|
+
attempt();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
beforeAll(async () => {
|
|
77
|
+
if (SKIP) return;
|
|
78
|
+
|
|
79
|
+
// Compile Java
|
|
80
|
+
fs.mkdirSync(BUILD_DIR, { recursive: true });
|
|
81
|
+
execSync(`javac -cp "${PY4J_JAR}" -d "${BUILD_DIR}" "${JAVA_SRC}"`, { stdio: 'pipe' });
|
|
82
|
+
|
|
83
|
+
// Start gateway
|
|
84
|
+
await new Promise((resolve, reject) => {
|
|
85
|
+
gatewayProcess = spawn('java', [
|
|
86
|
+
'-cp', `${BUILD_DIR}:${PY4J_JAR}`,
|
|
87
|
+
'TestEntryPoint',
|
|
88
|
+
String(gatewayPort),
|
|
89
|
+
], { stdio: ['pipe', 'pipe', 'inherit'] });
|
|
90
|
+
|
|
91
|
+
let buf = '';
|
|
92
|
+
gatewayProcess.stdout.setEncoding('utf8');
|
|
93
|
+
gatewayProcess.stdout.on('data', (chunk) => {
|
|
94
|
+
buf += chunk;
|
|
95
|
+
if (buf.includes('GATEWAY_STARTED:')) resolve();
|
|
96
|
+
});
|
|
97
|
+
gatewayProcess.on('error', reject);
|
|
98
|
+
setTimeout(() => reject(new Error('Gateway startup timeout')), 15000);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await waitForPort(gatewayPort);
|
|
102
|
+
|
|
103
|
+
gateway = new JavaGateway(new GatewayParameters({ port: gatewayPort }));
|
|
104
|
+
await gateway.connect();
|
|
105
|
+
}, 30000);
|
|
106
|
+
|
|
107
|
+
afterAll(async () => {
|
|
108
|
+
if (gateway) await gateway.close();
|
|
109
|
+
if (gatewayProcess) {
|
|
110
|
+
gatewayProcess.stdin.end();
|
|
111
|
+
gatewayProcess.kill('SIGTERM');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Conditionally skip if no Java/py4j available
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
const it2 = SKIP ? it.skip : it;
|
|
120
|
+
if (SKIP) {
|
|
121
|
+
console.warn(
|
|
122
|
+
'\n[SKIP] Integration tests require py4j JAR. ' +
|
|
123
|
+
'Install py4j (pip install py4j) or set PY4J_JAR=/path/to/py4j.jar\n'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Tests
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
describe('Integration: arithmetic', () => {
|
|
132
|
+
it2('add two integers', async () => {
|
|
133
|
+
const result = await gateway.entry_point.add(3, 4);
|
|
134
|
+
expect(result).toBe(7);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it2('add negative integers', async () => {
|
|
138
|
+
expect(await gateway.entry_point.add(-10, 5)).toBe(-5);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it2('add doubles', async () => {
|
|
142
|
+
expect(await gateway.entry_point.addDoubles(1.5, 2.5)).toBeCloseTo(4.0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it2('multiply', async () => {
|
|
146
|
+
expect(await gateway.entry_point.multiply(6, 7)).toBe(42);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it2('divide', async () => {
|
|
150
|
+
// 10.0 and 4.0 are integers in JS (Number.isInteger(10.0) === true) and encode
|
|
151
|
+
// as INTEGER_TYPE; Java's divide(double,double) only accepts doubles.
|
|
152
|
+
// Use non-integer values so they encode as DOUBLE_TYPE.
|
|
153
|
+
expect(await gateway.entry_point.divide(5.5, 2.2)).toBeCloseTo(2.5);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('Integration: strings', () => {
|
|
158
|
+
it2('greet returns correct string', async () => {
|
|
159
|
+
expect(await gateway.entry_point.greet('World')).toBe('Hello, World!');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it2('concatenate strings', async () => {
|
|
163
|
+
expect(await gateway.entry_point.concatenate('foo', 'bar')).toBe('foobar');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it2('string length', async () => {
|
|
167
|
+
expect(await gateway.entry_point.stringLength('hello')).toBe(5);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it2('toUpperCase', async () => {
|
|
171
|
+
expect(await gateway.entry_point.toUpperCase('hello')).toBe('HELLO');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it2('containsSubstring true', async () => {
|
|
175
|
+
expect(await gateway.entry_point.containsSubstring('foobar', 'oba')).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it2('containsSubstring false', async () => {
|
|
179
|
+
expect(await gateway.entry_point.containsSubstring('foobar', 'xyz')).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it2('repeatString', async () => {
|
|
183
|
+
expect(await gateway.entry_point.repeatString('ab', 3)).toBe('ababab');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('Integration: booleans', () => {
|
|
188
|
+
it2('andBool(true, true)', async () => {
|
|
189
|
+
expect(await gateway.entry_point.andBool(true, true)).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
it2('andBool(true, false)', async () => {
|
|
192
|
+
expect(await gateway.entry_point.andBool(true, false)).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
it2('orBool(false, true)', async () => {
|
|
195
|
+
expect(await gateway.entry_point.orBool(false, true)).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
it2('notBool(true)', async () => {
|
|
198
|
+
expect(await gateway.entry_point.notBool(true)).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('Integration: null handling', () => {
|
|
203
|
+
it2('maybeNull(true) returns null', async () => {
|
|
204
|
+
expect(await gateway.entry_point.maybeNull(true)).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it2('maybeNull(false) returns string', async () => {
|
|
208
|
+
expect(await gateway.entry_point.maybeNull(false)).toBe('not null');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('Integration: type round-trips', () => {
|
|
213
|
+
it2('echo integer', async () => {
|
|
214
|
+
expect(await gateway.entry_point.echoInt(42)).toBe(42);
|
|
215
|
+
});
|
|
216
|
+
it2('echo negative integer', async () => {
|
|
217
|
+
expect(await gateway.entry_point.echoInt(-99)).toBe(-99);
|
|
218
|
+
});
|
|
219
|
+
it2('echo long', async () => {
|
|
220
|
+
expect(await gateway.entry_point.echoLong(1000000000000)).toBe(1000000000000);
|
|
221
|
+
});
|
|
222
|
+
it2('echo double', async () => {
|
|
223
|
+
expect(await gateway.entry_point.echoDouble(3.14)).toBeCloseTo(3.14);
|
|
224
|
+
});
|
|
225
|
+
it2('echo boolean true', async () => {
|
|
226
|
+
expect(await gateway.entry_point.echoBool(true)).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
it2('echo boolean false', async () => {
|
|
229
|
+
expect(await gateway.entry_point.echoBool(false)).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
it2('echo string', async () => {
|
|
232
|
+
expect(await gateway.entry_point.echoString('js4j')).toBe('js4j');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('Integration: Java collections', () => {
|
|
237
|
+
it2('getStringList() size', async () => {
|
|
238
|
+
const list = await gateway.entry_point.getStringList();
|
|
239
|
+
expect(await list.size()).toBe(3);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it2('getStringList() get(0)', async () => {
|
|
243
|
+
const list = await gateway.entry_point.getStringList();
|
|
244
|
+
expect(await list.get(0)).toBe('alpha');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it2('getStringList() get(2)', async () => {
|
|
248
|
+
const list = await gateway.entry_point.getStringList();
|
|
249
|
+
expect(await list.get(2)).toBe('gamma');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it2('getIntList() get(4)', async () => {
|
|
253
|
+
const list = await gateway.entry_point.getIntList();
|
|
254
|
+
expect(await list.get(4)).toBe(5);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it2('getStringSet() size', async () => {
|
|
258
|
+
const set = await gateway.entry_point.getStringSet();
|
|
259
|
+
expect(await set.size()).toBe(3);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it2('getStringSet() contains existing', async () => {
|
|
263
|
+
const set = await gateway.entry_point.getStringSet();
|
|
264
|
+
expect(await set.contains('one')).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it2('getStringSet() contains missing', async () => {
|
|
268
|
+
const set = await gateway.entry_point.getStringSet();
|
|
269
|
+
expect(await set.contains('xxx')).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it2('getStringIntMap() size', async () => {
|
|
273
|
+
const map = await gateway.entry_point.getStringIntMap();
|
|
274
|
+
expect(await map.size()).toBe(3);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it2('getStringIntMap() get("a")', async () => {
|
|
278
|
+
const map = await gateway.entry_point.getStringIntMap();
|
|
279
|
+
expect(await map.get('a')).toBe(1);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it2('getStringIntMap() containsKey("a")', async () => {
|
|
283
|
+
const map = await gateway.entry_point.getStringIntMap();
|
|
284
|
+
expect(await map.containsKey('a')).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it2('getStringIntMap() containsKey("z")', async () => {
|
|
288
|
+
const map = await gateway.entry_point.getStringIntMap();
|
|
289
|
+
expect(await map.containsKey('z')).toBe(false);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('Integration: Java object (Counter)', () => {
|
|
294
|
+
it2('createCounter returns object with getValue', async () => {
|
|
295
|
+
const c = await gateway.entry_point.createCounter(10);
|
|
296
|
+
expect(await c.getValue()).toBe(10);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it2('increment increases value', async () => {
|
|
300
|
+
const c = await gateway.entry_point.createCounter(5);
|
|
301
|
+
await c.increment();
|
|
302
|
+
expect(await c.getValue()).toBe(6);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it2('add increases value by n', async () => {
|
|
306
|
+
const c = await gateway.entry_point.createCounter(3);
|
|
307
|
+
await c.add(7);
|
|
308
|
+
expect(await c.getValue()).toBe(10);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it2('decrement decreases value', async () => {
|
|
312
|
+
const c = await gateway.entry_point.createCounter(5);
|
|
313
|
+
await c.decrement();
|
|
314
|
+
expect(await c.getValue()).toBe(4);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it2('reset sets value to 0', async () => {
|
|
318
|
+
const c = await gateway.entry_point.createCounter(100);
|
|
319
|
+
await c.reset();
|
|
320
|
+
expect(await c.getValue()).toBe(0);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('Integration: exceptions', () => {
|
|
325
|
+
it2('throwException raises Js4JJavaError', async () => {
|
|
326
|
+
await expect(gateway.entry_point.throwException('boom')).rejects.toThrow(Js4JJavaError);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it2('divideByZero raises Js4JJavaError', async () => {
|
|
330
|
+
await expect(gateway.entry_point.divideInts(10, 0)).rejects.toThrow(Js4JJavaError);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it2('exception message contains Java details', async () => {
|
|
334
|
+
try {
|
|
335
|
+
await gateway.entry_point.throwException('test error message');
|
|
336
|
+
} catch (err) {
|
|
337
|
+
expect(err.javaExceptionMessage).toBeTruthy();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe('Integration: JVM namespace', () => {
|
|
343
|
+
it2('Math.abs(-42)', async () => {
|
|
344
|
+
expect(await gateway.jvm.java.lang.Math.abs(-42)).toBe(42);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it2('Math.max(3, 7)', async () => {
|
|
348
|
+
expect(await gateway.jvm.java.lang.Math.max(3, 7)).toBe(7);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it2('Math.min(3, 7)', async () => {
|
|
352
|
+
expect(await gateway.jvm.java.lang.Math.min(3, 7)).toBe(3);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it2('String.valueOf(123)', async () => {
|
|
356
|
+
expect(await gateway.jvm.java.lang.String.valueOf(123)).toBe('123');
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe('Integration: StringBuilder (constructor via JVM)', () => {
|
|
361
|
+
it2('create and append', async () => {
|
|
362
|
+
const sb = await gateway.jvm.java.lang.StringBuilder('Hello');
|
|
363
|
+
await sb.append(' World');
|
|
364
|
+
expect(await sb.toString()).toBe('Hello World');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it2('chain multiple appends', async () => {
|
|
368
|
+
const sb = await gateway.jvm.java.lang.StringBuilder();
|
|
369
|
+
await sb.append('a');
|
|
370
|
+
await sb.append('b');
|
|
371
|
+
await sb.append('c');
|
|
372
|
+
expect(await sb.toString()).toBe('abc');
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe('Integration: ArrayList (constructor via JVM)', () => {
|
|
377
|
+
it2('create and add elements', async () => {
|
|
378
|
+
const list = await gateway.jvm.java.util.ArrayList();
|
|
379
|
+
await list.add('x');
|
|
380
|
+
await list.add('y');
|
|
381
|
+
expect(await list.size()).toBe(2);
|
|
382
|
+
expect(await list.get(0)).toBe('x');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
createJavaList,
|
|
5
|
+
createJavaSet,
|
|
6
|
+
createJavaMap,
|
|
7
|
+
createJavaArray,
|
|
8
|
+
createJavaIterator,
|
|
9
|
+
} = require('../../src/collections');
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Mock gateway client
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
function makeMockClient(responses = {}) {
|
|
16
|
+
return {
|
|
17
|
+
async callMethod(targetId, methodName, args) {
|
|
18
|
+
const key = `${targetId}.${methodName}`;
|
|
19
|
+
if (key in responses) {
|
|
20
|
+
const val = responses[key];
|
|
21
|
+
return typeof val === 'function' ? val(args) : val;
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`Mock: unexpected callMethod(${targetId}, ${methodName}, ${JSON.stringify(args)})`);
|
|
24
|
+
},
|
|
25
|
+
async getField(targetId, fieldName) {
|
|
26
|
+
const key = `${targetId}.${fieldName}`;
|
|
27
|
+
if (key in responses) return responses[key];
|
|
28
|
+
throw new Error(`Mock: unexpected getField(${targetId}, ${fieldName})`);
|
|
29
|
+
},
|
|
30
|
+
async _sendCommand(cmd) { return 'yv'; },
|
|
31
|
+
_wrapObject: (id) => ({ _targetId: id }),
|
|
32
|
+
_lookupProxy: () => null,
|
|
33
|
+
_proxyPool: null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// JavaList
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
describe('JavaList', () => {
|
|
42
|
+
test('size() delegates to callMethod', async () => {
|
|
43
|
+
const client = makeMockClient({ 'list1.size': 3 });
|
|
44
|
+
const list = createJavaList('list1', client);
|
|
45
|
+
expect(await list.size()).toBe(3);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('get(index) delegates to callMethod', async () => {
|
|
49
|
+
const client = makeMockClient({ 'list1.get': (args) => ['a', 'b', 'c'][args[0]] });
|
|
50
|
+
const list = createJavaList('list1', client);
|
|
51
|
+
expect(await list.get(0)).toBe('a');
|
|
52
|
+
expect(await list.get(2)).toBe('c');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('add(element) delegates to callMethod', async () => {
|
|
56
|
+
const added = [];
|
|
57
|
+
const client = makeMockClient({
|
|
58
|
+
'list1.add': (args) => { added.push(args[0]); return true; },
|
|
59
|
+
});
|
|
60
|
+
const list = createJavaList('list1', client);
|
|
61
|
+
await list.add('hello');
|
|
62
|
+
expect(added).toEqual(['hello']);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('clear() delegates to callMethod', async () => {
|
|
66
|
+
let cleared = false;
|
|
67
|
+
const client = makeMockClient({ 'list1.clear': () => { cleared = true; return null; } });
|
|
68
|
+
const list = createJavaList('list1', client);
|
|
69
|
+
await list.clear();
|
|
70
|
+
expect(cleared).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('contains() returns correct value', async () => {
|
|
74
|
+
const client = makeMockClient({
|
|
75
|
+
'list1.contains': (args) => args[0] === 'x',
|
|
76
|
+
});
|
|
77
|
+
const list = createJavaList('list1', client);
|
|
78
|
+
expect(await list.contains('x')).toBe(true);
|
|
79
|
+
expect(await list.contains('y')).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('toArray() collects all elements', async () => {
|
|
83
|
+
const data = ['a', 'b', 'c'];
|
|
84
|
+
const client = makeMockClient({
|
|
85
|
+
'list1.size': data.length,
|
|
86
|
+
'list1.get': (args) => data[args[0]],
|
|
87
|
+
});
|
|
88
|
+
const list = createJavaList('list1', client);
|
|
89
|
+
expect(await list.toArray()).toEqual(['a', 'b', 'c']);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('async iterator yields elements in order', async () => {
|
|
93
|
+
const data = [10, 20, 30];
|
|
94
|
+
const client = makeMockClient({
|
|
95
|
+
'list1.size': data.length,
|
|
96
|
+
'list1.get': (args) => data[args[0]],
|
|
97
|
+
});
|
|
98
|
+
const list = createJavaList('list1', client);
|
|
99
|
+
const collected = [];
|
|
100
|
+
for await (const item of list) {
|
|
101
|
+
collected.push(item);
|
|
102
|
+
}
|
|
103
|
+
expect(collected).toEqual([10, 20, 30]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('_targetId is accessible', () => {
|
|
107
|
+
const list = createJavaList('myListId', makeMockClient());
|
|
108
|
+
expect(list._targetId).toBe('myListId');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// JavaSet
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
describe('JavaSet', () => {
|
|
117
|
+
test('size() delegates to callMethod', async () => {
|
|
118
|
+
const client = makeMockClient({ 'set1.size': 2 });
|
|
119
|
+
const set = createJavaSet('set1', client);
|
|
120
|
+
expect(await set.size()).toBe(2);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('contains() returns correct value', async () => {
|
|
124
|
+
const client = makeMockClient({
|
|
125
|
+
'set1.contains': (args) => args[0] === 'present',
|
|
126
|
+
});
|
|
127
|
+
const set = createJavaSet('set1', client);
|
|
128
|
+
expect(await set.contains('present')).toBe(true);
|
|
129
|
+
expect(await set.contains('absent')).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('add() delegates to callMethod', async () => {
|
|
133
|
+
const added = [];
|
|
134
|
+
const client = makeMockClient({
|
|
135
|
+
'set1.add': (args) => { added.push(args[0]); return true; },
|
|
136
|
+
});
|
|
137
|
+
const set = createJavaSet('set1', client);
|
|
138
|
+
await set.add('newItem');
|
|
139
|
+
expect(added).toContain('newItem');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('_isJavaSet flag is set', () => {
|
|
143
|
+
const set = createJavaSet('s1', makeMockClient());
|
|
144
|
+
expect(set._isJavaSet).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// JavaMap
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
describe('JavaMap', () => {
|
|
153
|
+
test('size() delegates to callMethod', async () => {
|
|
154
|
+
const client = makeMockClient({ 'map1.size': 3 });
|
|
155
|
+
const map = createJavaMap('map1', client);
|
|
156
|
+
expect(await map.size()).toBe(3);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('get(key) returns value', async () => {
|
|
160
|
+
const store = { a: 1, b: 2 };
|
|
161
|
+
const client = makeMockClient({
|
|
162
|
+
'map1.get': (args) => store[args[0]] ?? null,
|
|
163
|
+
});
|
|
164
|
+
const map = createJavaMap('map1', client);
|
|
165
|
+
expect(await map.get('a')).toBe(1);
|
|
166
|
+
expect(await map.get('b')).toBe(2);
|
|
167
|
+
expect(await map.get('z')).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('put(key, value) delegates to callMethod', async () => {
|
|
171
|
+
const puts = [];
|
|
172
|
+
const client = makeMockClient({
|
|
173
|
+
'map1.put': (args) => { puts.push(args); return null; },
|
|
174
|
+
});
|
|
175
|
+
const map = createJavaMap('map1', client);
|
|
176
|
+
await map.put('key', 42);
|
|
177
|
+
expect(puts[0]).toEqual(['key', 42]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('containsKey() returns correct value', async () => {
|
|
181
|
+
const client = makeMockClient({
|
|
182
|
+
'map1.containsKey': (args) => args[0] === 'existing',
|
|
183
|
+
});
|
|
184
|
+
const map = createJavaMap('map1', client);
|
|
185
|
+
expect(await map.containsKey('existing')).toBe(true);
|
|
186
|
+
expect(await map.containsKey('missing')).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('_isJavaMap flag is set', () => {
|
|
190
|
+
const map = createJavaMap('m1', makeMockClient());
|
|
191
|
+
expect(map._isJavaMap).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// JavaArray
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
// JavaArray now uses _sendCommand with the ARRAY protocol (a\n) rather than
|
|
200
|
+
// callMethod. The mock client must intercept _sendCommand and return a
|
|
201
|
+
// properly-encoded response line.
|
|
202
|
+
describe('JavaArray', () => {
|
|
203
|
+
const { ARRAY_COMMAND_NAME, ARRAY_GET_SUB_COMMAND_NAME, ARRAY_LEN_SUB_COMMAND_NAME } =
|
|
204
|
+
require('../../src/protocol');
|
|
205
|
+
|
|
206
|
+
function makeArrayClient(responses) {
|
|
207
|
+
// responses: array of { test: (cmd) => bool, answer: string }
|
|
208
|
+
return {
|
|
209
|
+
async _sendCommand(cmd) {
|
|
210
|
+
for (const { test, answer } of responses) {
|
|
211
|
+
if (test(cmd)) return answer;
|
|
212
|
+
}
|
|
213
|
+
return 'yv'; // default void
|
|
214
|
+
},
|
|
215
|
+
_wrapObject: (id) => ({ _targetId: id }),
|
|
216
|
+
_lookupProxy: () => null,
|
|
217
|
+
_proxyPool: null,
|
|
218
|
+
// callMethod still needed for the `call()` passthrough
|
|
219
|
+
async callMethod(targetId, method, args) { return null; },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
test('get(index) uses ARRAY GET sub-command', async () => {
|
|
224
|
+
const GET_PREFIX = ARRAY_COMMAND_NAME + ARRAY_GET_SUB_COMMAND_NAME;
|
|
225
|
+
const client = makeArrayClient([
|
|
226
|
+
{ test: (cmd) => cmd.includes(GET_PREFIX), answer: 'yi200' },
|
|
227
|
+
]);
|
|
228
|
+
const arr = createJavaArray('arr1', client);
|
|
229
|
+
expect(await arr.get(1)).toBe(200);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('length() uses ARRAY LEN sub-command', async () => {
|
|
233
|
+
const LEN_PREFIX = ARRAY_COMMAND_NAME + ARRAY_LEN_SUB_COMMAND_NAME;
|
|
234
|
+
const client = makeArrayClient([
|
|
235
|
+
{ test: (cmd) => cmd.includes(LEN_PREFIX), answer: 'yi5' },
|
|
236
|
+
]);
|
|
237
|
+
const arr = createJavaArray('arr1', client);
|
|
238
|
+
expect(await arr.length()).toBe(5);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('toArray() collects all elements via get()', async () => {
|
|
242
|
+
const data = [10, 20, 30];
|
|
243
|
+
// Match the FULL prefix (ARRAY_COMMAND_NAME + sub-command) to avoid false
|
|
244
|
+
// positives: ARRAY_LEN_SUB_COMMAND_NAME = 'e\n' also appears as the END
|
|
245
|
+
// marker at the tail of every command string.
|
|
246
|
+
const LEN_PREFIX = ARRAY_COMMAND_NAME + ARRAY_LEN_SUB_COMMAND_NAME; // 'a\ne\n'
|
|
247
|
+
const GET_PREFIX = ARRAY_COMMAND_NAME + ARRAY_GET_SUB_COMMAND_NAME; // 'a\ng\n'
|
|
248
|
+
let calls = 0;
|
|
249
|
+
const client = makeMockClient();
|
|
250
|
+
client._sendCommand = async (cmd) => {
|
|
251
|
+
if (cmd.includes(LEN_PREFIX)) return `yi${data.length}`;
|
|
252
|
+
if (cmd.includes(GET_PREFIX)) return `yi${data[calls++]}`;
|
|
253
|
+
return 'yv';
|
|
254
|
+
};
|
|
255
|
+
const arr = createJavaArray('arr1', client);
|
|
256
|
+
expect(await arr.toArray()).toEqual([10, 20, 30]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('_isJavaArray flag is set', () => {
|
|
260
|
+
expect(createJavaArray('a1', makeMockClient())._isJavaArray).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// JavaIterator
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
describe('JavaIterator', () => {
|
|
269
|
+
test('iterates via hasNext/next', async () => {
|
|
270
|
+
const items = ['x', 'y', 'z'];
|
|
271
|
+
let idx = 0;
|
|
272
|
+
const client = makeMockClient({
|
|
273
|
+
'iter1.hasNext': () => idx < items.length,
|
|
274
|
+
'iter1.next': () => items[idx++],
|
|
275
|
+
});
|
|
276
|
+
const iter = createJavaIterator('iter1', client);
|
|
277
|
+
const collected = [];
|
|
278
|
+
while (await iter.hasNext()) {
|
|
279
|
+
collected.push(await iter.next());
|
|
280
|
+
}
|
|
281
|
+
expect(collected).toEqual(['x', 'y', 'z']);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('async iterator protocol works', async () => {
|
|
285
|
+
const items = [1, 2, 3];
|
|
286
|
+
let idx = 0;
|
|
287
|
+
const client = makeMockClient({
|
|
288
|
+
'iter1.hasNext': () => idx < items.length,
|
|
289
|
+
'iter1.next': () => items[idx++],
|
|
290
|
+
});
|
|
291
|
+
const iter = createJavaIterator('iter1', client);
|
|
292
|
+
const collected = [];
|
|
293
|
+
for await (const item of iter) {
|
|
294
|
+
collected.push(item);
|
|
295
|
+
}
|
|
296
|
+
expect(collected).toEqual([1, 2, 3]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('toArray() returns all remaining elements', async () => {
|
|
300
|
+
const items = ['a', 'b'];
|
|
301
|
+
let idx = 0;
|
|
302
|
+
const client = makeMockClient({
|
|
303
|
+
'iter1.hasNext': () => idx < items.length,
|
|
304
|
+
'iter1.next': () => items[idx++],
|
|
305
|
+
});
|
|
306
|
+
const iter = createJavaIterator('iter1', client);
|
|
307
|
+
expect(await iter.toArray()).toEqual(['a', 'b']);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('_isJavaIterator flag is set', () => {
|
|
311
|
+
expect(createJavaIterator('i1', makeMockClient())._isJavaIterator).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
});
|