1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.jexl3;
18
19 import static org.junit.jupiter.api.Assertions.assertEquals;
20 import static org.junit.jupiter.api.Assertions.assertFalse;
21 import static org.junit.jupiter.api.Assertions.assertNotEquals;
22 import static org.junit.jupiter.api.Assertions.assertThrows;
23 import static org.junit.jupiter.api.Assertions.assertTrue;
24
25 import java.time.Duration;
26 import java.util.List;
27 import java.util.concurrent.Callable;
28 import java.util.concurrent.CancellationException;
29 import java.util.concurrent.ExecutionException;
30 import java.util.concurrent.ExecutorService;
31 import java.util.concurrent.Executors;
32 import java.util.concurrent.Future;
33 import java.util.concurrent.FutureTask;
34 import java.util.concurrent.Semaphore;
35 import java.util.concurrent.TimeUnit;
36 import java.util.concurrent.TimeoutException;
37 import java.util.concurrent.atomic.AtomicBoolean;
38
39 import org.apache.commons.jexl3.internal.Script;
40 import org.apache.commons.lang3.ThreadUtils;
41 import org.junit.jupiter.api.Test;
42
43
44
45
46 @SuppressWarnings({ "UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes" })
47 class ScriptCallableTest extends JexlTestCase {
48 public static class AnnotationContext extends MapContext implements JexlContext.AnnotationProcessor {
49 @Override
50 public Object processAnnotation(final String name, final Object[] args, final Callable<Object> statement) throws Exception {
51 if ("timeout".equals(name) && args != null && args.length > 0) {
52 final long ms = args[0] instanceof Number ? ((Number) args[0]).longValue() : Long.parseLong(args[0].toString());
53 final Object def = args.length > 1 ? args[1] : null;
54 if (ms > 0) {
55 final ExecutorService executor = Executors.newFixedThreadPool(1);
56 Future<?> future = null;
57 try {
58 future = executor.submit(statement);
59 return future.get(ms, TimeUnit.MILLISECONDS);
60 } catch (final TimeoutException xtimeout) {
61 future.cancel(true);
62 } finally {
63 executor.shutdown();
64 }
65
66 }
67 return def;
68 }
69 return statement.call();
70 }
71
72 public void sleep(final long ms) throws InterruptedException {
73 Thread.sleep(ms);
74 }
75
76 }
77
78 public static class CancellationContext extends MapContext implements JexlContext.CancellationHandle {
79 private final AtomicBoolean cancellation;
80
81 CancellationContext(final AtomicBoolean c) {
82 cancellation = c;
83 }
84
85 @Override
86 public AtomicBoolean getCancellation() {
87 return cancellation;
88 }
89 }
90
91 public static class TestContext extends MapContext implements JexlContext.NamespaceResolver {
92 public int hangs(final Object t) {
93 return 1;
94 }
95
96 public int interrupt() {
97 Thread.currentThread().interrupt();
98 return 42;
99 }
100
101 @Override
102 public Object resolveNamespace(final String name) {
103 return name == null ? this : null;
104 }
105
106 public int runForever() {
107 while (true) {
108 if (Thread.currentThread().isInterrupted()) {
109 break;
110 }
111 }
112 return 1;
113 }
114
115 public void sleep(final long millis) throws InterruptedException {
116 Thread.sleep(millis);
117 }
118
119 public int wait(final int s) throws InterruptedException {
120 Thread.sleep(1000 * s);
121 return s;
122 }
123
124 public int waitInterrupt(final int s) {
125 try {
126 Thread.sleep(1000 * s);
127 return s;
128 } catch (final InterruptedException xint) {
129 Thread.currentThread().interrupt();
130 }
131 return -1;
132 }
133 }
134
135
136 public ScriptCallableTest() {
137 super("ScriptCallableTest");
138 }
139
140
141
142
143
144
145 private void runInterrupt(final JexlEngine jexl) throws Exception {
146 List<Runnable> lr = null;
147 final ExecutorService executorService = Executors.newFixedThreadPool(2);
148 try {
149 final JexlContext ctxt = new TestContext();
150
151
152 final JexlScript sint = jexl.createScript("interrupt(); return 42");
153 Object t = null;
154 Script.Callable c = (Script.Callable) sint.callable(ctxt);
155 try {
156 t = c.call();
157 assertFalse(c.isCancellable(), "should have thrown a Cancel");
158 } catch (final JexlException.Cancel xjexl) {
159 assertTrue(c.isCancellable(), () -> "should not have thrown " + xjexl);
160 }
161 assertTrue(c.isCancelled());
162 assertNotEquals(42, t);
163
164
165 c = (Script.Callable) sint.callable(ctxt);
166 final Future<Object> f = executorService.submit(c);
167 try {
168 t = f.get();
169 assertFalse(c.isCancellable(), "should have thrown a Cancel");
170 } catch (final ExecutionException xexec) {
171 assertTrue(c.isCancellable(), () -> "should not have thrown " + xexec);
172 }
173 assertTrue(c.isCancelled());
174 assertNotEquals(42, t);
175
176
177 final JexlScript ssleep = jexl.createScript("sleep(30000); return 42");
178 final Future<Object> f0 = executorService.submit(ssleep.callable(ctxt));
179 assertThrows(TimeoutException.class, () -> f0.get(100L, TimeUnit.MILLISECONDS));
180 f0.cancel(true);
181 assertNotEquals(42, t);
182
183
184 final Future<Object> fc0 = executorService.submit(ssleep.callable(ctxt));
185 final Runnable cancels0 = () -> {
186 ThreadUtils.sleepQuietly(Duration.ofMillis(200));
187 fc0.cancel(true);
188 };
189 executorService.submit(cancels0);
190 assertThrows(CancellationException.class, () -> f0.get(100L, TimeUnit.MILLISECONDS));
191
192
193 final JexlScript swhile = jexl.createScript("while(true); return 42");
194 final Future<Object> f1 = executorService.submit(swhile.callable(ctxt));
195 assertThrows(TimeoutException.class, () -> f1.get(100L, TimeUnit.MILLISECONDS));
196 f1.cancel(true);
197
198 assertNotEquals(42, t);
199
200
201 final Future<Object> fc = executorService.submit(swhile.callable(ctxt));
202 final Runnable cancels = () -> {
203 ThreadUtils.sleepQuietly(Duration.ofMillis(200));
204 fc.cancel(true);
205 };
206 executorService.submit(cancels);
207 assertThrows(CancellationException.class, fc::get);
208 assertNotEquals(42, t);
209 } finally {
210 lr = executorService.shutdownNow();
211 }
212 assertTrue(lr.isEmpty());
213 }
214
215 @Test
216 void testCallableCancel() throws Exception {
217 final Semaphore latch = new Semaphore(0);
218 final JexlContext ctxt = new MapContext();
219 ctxt.set("latch", latch);
220
221 final JexlScript e = JEXL.createScript("latch.release(); while(true);");
222 final Script.Callable c = (Script.Callable) e.callable(ctxt);
223 final Callable<Object> kc = () -> {
224 latch.acquire();
225 return c.cancel();
226 };
227 final List<Runnable> list;
228 final ExecutorService executor = Executors.newFixedThreadPool(2);
229 try {
230 final Future<?> future = executor.submit(c);
231 final Future<?> kfc = executor.submit(kc);
232 assertTrue((Boolean) kfc.get());
233 final ExecutionException xexec = assertThrows(ExecutionException.class, future::get);
234 assertTrue(xexec.getCause() instanceof JexlException.Cancel);
235 } finally {
236 list = executor.shutdownNow();
237 }
238 assertTrue(c.isCancelled());
239 assertTrue(list == null || list.isEmpty());
240 }
241
242
243
244
245 @Test
246 void testCallableCancellation() throws Exception {
247 final Semaphore latch = new Semaphore(0);
248 final AtomicBoolean cancel = new AtomicBoolean();
249 final JexlContext ctxt = new CancellationContext(cancel);
250 ctxt.set("latch", latch);
251
252 final JexlScript e = JEXL.createScript("latch.release(); while(true);");
253 final Script.Callable c = (Script.Callable) e.callable(ctxt);
254 final Callable<Object> kc = () -> {
255 latch.acquire();
256 return cancel.compareAndSet(false, true);
257 };
258 final List<Runnable> list;
259 final ExecutorService executor = Executors.newFixedThreadPool(2);
260 try {
261 final Future<?> future = executor.submit(c);
262 final Future<?> kfc = executor.submit(kc);
263 assertTrue((Boolean) kfc.get());
264 final ExecutionException xexec = assertThrows(ExecutionException.class, future::get);
265 assertTrue(xexec.getCause() instanceof JexlException.Cancel);
266 } finally {
267 list = executor.shutdownNow();
268 }
269 assertTrue(c.isCancelled());
270 assertTrue(list == null || list.isEmpty());
271 }
272
273 @Test
274 void testCallableClosure() throws Exception {
275 final JexlScript e = JEXL.createScript("function(t) {while(t);}");
276 final Callable<Object> c = e.callable(null, Boolean.TRUE);
277 final List<Runnable> list;
278 final ExecutorService executor = Executors.newFixedThreadPool(1);
279 final Future<?> future;
280 try {
281 future = executor.submit(c);
282 assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
283 future.cancel(true);
284 } finally {
285 list = executor.shutdownNow();
286 }
287 assertTrue(future.isCancelled());
288 assertTrue(list.isEmpty());
289 }
290
291 @Test
292 void testCallableTimeout() throws Exception {
293 final Semaphore latch = new Semaphore(0);
294 final JexlContext ctxt = new MapContext();
295 ctxt.set("latch", latch);
296 final JexlScript e = JEXL.createScript("latch.release(); while(true);");
297 final Callable<Object> c = e.callable(ctxt);
298 final List<Runnable> list;
299 final ExecutorService executor = Executors.newFixedThreadPool(1);
300 final Future<?> future;
301 try {
302 future = executor.submit(c);
303 latch.acquire();
304 assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
305 future.cancel(true);
306 } finally {
307 list = executor.shutdownNow();
308 }
309 assertTrue(future.isCancelled());
310 assertTrue(list.isEmpty());
311 }
312
313 @Test
314 void testCancelForever() throws Exception {
315 final Semaphore latch = new Semaphore(0);
316 final JexlContext ctxt = new TestContext();
317 ctxt.set("latch", latch);
318
319 final JexlScript e = JEXL.createScript("latch.release(); runForever()");
320 final Callable<Object> c = e.callable(ctxt);
321
322 final List<Runnable> list;
323 final ExecutorService executor = Executors.newFixedThreadPool(1);
324 final Future<?> future;
325 try {
326 future = executor.submit(c);
327 assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
328 future.cancel(true);
329 } finally {
330 list = executor.shutdownNow();
331 }
332 assertTrue(future.isCancelled());
333 assertTrue(list.isEmpty());
334 }
335
336 @Test
337 void testCancelLoopWait() throws Exception {
338 final List<Runnable> list;
339 final JexlScript e = JEXL.createScript("while (true) { wait(10) }");
340 final Callable<Object> c = e.callable(new TestContext());
341 final ExecutorService executor = Executors.newFixedThreadPool(1);
342 final Future<?> future;
343 try {
344 future = executor.submit(c);
345 assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
346 future.cancel(true);
347 } finally {
348 list = executor.shutdownNow();
349 }
350 assertTrue(future.isCancelled());
351 assertTrue(list.isEmpty());
352 }
353
354 @Test
355 void testCancelWait() throws Exception {
356 final List<Runnable> list;
357 final JexlScript e = JEXL.createScript("wait(10)");
358 final Callable<Object> c = e.callable(new TestContext());
359 final ExecutorService executor = Executors.newFixedThreadPool(1);
360 try {
361 final Future<?> future = executor.submit(c);
362 assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
363 future.cancel(true);
364 assertTrue(future.isCancelled());
365 } finally {
366 list = executor.shutdownNow();
367 }
368 assertTrue(list.isEmpty());
369 }
370
371 @Test
372 void testCancelWaitInterrupt() throws Exception {
373 final List<Runnable> list;
374 final JexlScript e = JEXL.createScript("waitInterrupt(42)");
375 final Callable<Object> c = e.callable(new TestContext());
376 final ExecutorService executor = Executors.newFixedThreadPool(1);
377 final Future<?> future;
378 try {
379 future = executor.submit(c);
380 assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
381 future.cancel(true);
382 } finally {
383 list = executor.shutdownNow();
384 }
385 assertTrue(future.isCancelled());
386 assertTrue(list.isEmpty());
387 }
388
389 @Test
390 void testFuture() throws Exception {
391 final JexlScript e = JEXL.createScript("while(true);");
392 final FutureTask<Object> future = new FutureTask<>(e.callable(null));
393 final ExecutorService executor = Executors.newFixedThreadPool(1);
394 try {
395 executor.submit(future);
396 assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS));
397 future.cancel(true);
398 } finally {
399 executor.shutdown();
400 }
401 assertTrue(future.isCancelled());
402 }
403
404 @Test
405 void testHangs() throws Exception {
406 final JexlScript e = JEXL.createScript("hangs()");
407 final Callable<Object> c = e.callable(new TestContext());
408 final ExecutorService executor = Executors.newFixedThreadPool(1);
409 try {
410 final Future<?> future = executor.submit(c);
411 final ExecutionException xexec = assertThrows(ExecutionException.class, () -> future.get(1, TimeUnit.SECONDS));
412 assertTrue(xexec.getCause() instanceof JexlException.Method);
413 } finally {
414 executor.shutdown();
415 }
416 }
417
418 @Test
419 void testInterruptCancellable() throws Exception {
420 runInterrupt(new JexlBuilder().silent(true).strict(true).cancellable(true).create());
421 }
422
423 @Test
424 void testInterruptSilentLenient() throws Exception {
425 runInterrupt(new JexlBuilder().silent(true).strict(false).create());
426 }
427
428 @Test
429 void testInterruptSilentStrict() throws Exception {
430 runInterrupt(new JexlBuilder().silent(true).strict(true).create());
431 }
432
433 @Test
434 void testInterruptVerboseLenient() throws Exception {
435 runInterrupt(new JexlBuilder().silent(false).strict(false).create());
436 }
437
438 @Test
439 void testInterruptVerboseStrict() throws Exception {
440 runInterrupt(new JexlBuilder().silent(false).strict(true).create());
441 }
442
443 @Test
444 void testNoWait() throws Exception {
445 List<Runnable> lr = null;
446 final JexlScript e = JEXL.createScript("wait(0)");
447 final Callable<Object> c = e.callable(new TestContext());
448
449 final ExecutorService executor = Executors.newFixedThreadPool(1);
450 try {
451 final Future<?> future = executor.submit(c);
452 final Object t = future.get(2, TimeUnit.SECONDS);
453 assertTrue(future.isDone());
454 assertEquals(0, t);
455 } finally {
456 lr = executor.shutdownNow();
457 }
458 assertTrue(lr.isEmpty());
459 }
460
461 @Test
462 void testTimeout() throws Exception {
463 JexlScript script = JEXL.createScript("(flag)->{ @timeout(100) { while(flag); return 42 }; 'cancelled' }");
464 final JexlContext ctxt = new AnnotationContext();
465 Object result = script.execute(ctxt, true);
466 assertEquals("cancelled", result);
467
468 result = script.execute(ctxt, false);
469 assertEquals(42, result);
470 script = JEXL.createScript("(flag)->{ @timeout(100, 'cancelled') { while(flag); 42; } }");
471 result = script.execute(ctxt, true);
472 assertEquals("cancelled", result);
473
474 result = script.execute(ctxt, false);
475 assertEquals(42, result);
476 script = JEXL.createScript("@timeout(100) {sleep(1000); 42; } -42;");
477 result = script.execute(ctxt);
478 assertEquals(-42, result);
479
480 script = JEXL.createScript("@timeout(100) {sleep(1000); return 42; } return -42;");
481 result = script.execute(ctxt);
482 assertEquals(-42, result);
483 script = JEXL.createScript("@timeout(1000) {sleep(100); return 42; } return -42;");
484 result = script.execute(ctxt);
485 assertEquals(42, result);
486 }
487
488 @Test
489 void testWait() throws Exception {
490 List<Runnable> lr = null;
491 final JexlScript e = JEXL.createScript("wait(1)");
492 final Callable<Object> c = e.callable(new TestContext());
493
494 final ExecutorService executor = Executors.newFixedThreadPool(1);
495 try {
496 final Future<?> future = executor.submit(c);
497 final Object t = future.get(2, TimeUnit.SECONDS);
498 assertEquals(1, t);
499 } finally {
500 lr = executor.shutdownNow();
501 }
502 assertTrue(lr.isEmpty());
503 }
504 }