1 /*
  2  * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 /*
 25  * @test id=platform
 26  * @summary Basic tests for ThreadFlock
 27  * @modules java.base/jdk.internal.misc
 28  * @run junit/othervm -DthreadFactory=platform ThreadFlockTest
 29  */
 30 
 31 /*
 32  * @test id=virtual
 33  * @modules java.base/jdk.internal.misc
 34  * @run junit/othervm -DthreadFactory=virtual ThreadFlockTest
 35  */
 36 
 37 import java.time.Duration;
 38 import java.util.*;
 39 import java.util.function.Function;
 40 import java.util.concurrent.*;
 41 import java.util.concurrent.atomic.AtomicBoolean;
 42 import java.util.concurrent.atomic.AtomicReference;
 43 import java.util.stream.Collectors;
 44 import java.util.stream.Stream;
 45 import jdk.internal.misc.ThreadFlock;
 46 
 47 import org.junit.jupiter.api.Test;
 48 import org.junit.jupiter.api.BeforeAll;
 49 import org.junit.jupiter.api.AfterAll;
 50 import org.junit.jupiter.params.ParameterizedTest;
 51 import org.junit.jupiter.params.provider.MethodSource;
 52 import static org.junit.jupiter.api.Assertions.*;
 53 
 54 class ThreadFlockTest {
 55     private static ScheduledExecutorService scheduler;
 56     private static List<ThreadFactory> threadFactories;
 57 
 58     @BeforeAll
 59     static void setup() throws Exception {
 60         scheduler = Executors.newSingleThreadScheduledExecutor();
 61 
 62         // thread factories
 63         String value = System.getProperty("threadFactory");
 64         List<ThreadFactory> list = new ArrayList<>();
 65         if (value == null || value.equals("platform"))
 66             list.add(Thread.ofPlatform().factory());
 67         if (value == null || value.equals("virtual"))
 68             list.add(Thread.ofVirtual().factory());
 69         assertTrue(list.size() > 0, "No thread factories for tests");
 70         threadFactories = list;
 71     }
 72 
 73     @AfterAll
 74     static void shutdown() {
 75         scheduler.shutdown();
 76     }
 77 
 78     private static Stream<ThreadFactory> factories() {
 79         return threadFactories.stream();
 80     }
 81 
 82     /**
 83      * Test ThreadFlock::name.
 84      */
 85     @Test
 86     void testName() {
 87         try (var flock = ThreadFlock.open(null)) {
 88             assertNull(flock.name());
 89             flock.close();
 90             assertNull(flock.name());  // after close
 91         }
 92         try (var flock = ThreadFlock.open("fetcher")) {
 93             assertEquals("fetcher", flock.name());
 94             flock.close();
 95             assertEquals("fetcher", flock.name());  // after close
 96         }
 97     }
 98 
 99     /**
100      * Test ThreadFlock::owner.
101      */
102     @Test
103     void testOwner() {
104         try (var flock = ThreadFlock.open(null)) {
105             assertTrue(flock.owner() == Thread.currentThread());
106             flock.close();
107             assertTrue(flock.owner() == Thread.currentThread());  // after close
108         }
109     }
110 
111     /**
112      * Test ThreadFlock::isXXXX methods.
113      */
114     @Test
115     void testState() {
116         try (var flock = ThreadFlock.open(null)) {
117             assertFalse(flock.isShutdown());
118             assertFalse(flock.isClosed());
119             flock.close();
120             assertTrue(flock.isShutdown());
121             assertTrue(flock.isClosed());
122         }
123         try (var flock = ThreadFlock.open(null)) {
124             flock.shutdown();
125             assertTrue(flock.isShutdown());
126             assertFalse(flock.isClosed());
127             flock.close();
128             assertTrue(flock.isShutdown());
129             assertTrue(flock.isClosed());
130         }
131     }
132 
133     /**
134      * Test ThreadFlock::threads enumerates all threads.
135      */
136     @ParameterizedTest
137     @MethodSource("factories")
138     void testThreads(ThreadFactory factory) {
139         CountDownLatch latch = new CountDownLatch(1);
140         var exception = new AtomicReference<Exception>();
141         Runnable awaitLatch = () -> {
142             try {
143                 latch.await();
144             } catch (Exception e) {
145                 exception.compareAndSet(null, e);
146             }
147         };
148 
149         var flock = ThreadFlock.open(null);
150         try {
151             assertTrue(flock.threads().count() == 0);
152 
153             // start 100 threads
154             Set<Thread> threads = new HashSet<>();
155             for (int i = 0; i < 100; i++) {
156                 Thread thread = factory.newThread(awaitLatch);
157                 flock.start(thread);
158                 threads.add(thread);
159             }
160 
161             // check thread ThreadFlock::threads enumerates all threads
162             assertEquals(flock.threads().collect(Collectors.toSet()), threads);
163 
164         } finally {
165             latch.countDown();  // release threads
166             flock.close();
167         }
168         assertTrue(flock.threads().count() == 0);
169         assertNull(exception.get());
170     }
171 
172     /**
173      * Test ThreadFlock::containsThread with nested flocks.
174      */
175     @ParameterizedTest
176     @MethodSource("factories")
177     void testContainsThread1(ThreadFactory factory) {
178         CountDownLatch latch = new CountDownLatch(1);
179         var exception = new AtomicReference<Exception>();
180 
181         Runnable awaitLatch = () -> {
182             try {
183                 latch.await();
184             } catch (Exception e) {
185                 exception.compareAndSet(null, e);
186             }
187         };
188 
189         try (var flock1 = ThreadFlock.open(null)) {
190             var flock2 = ThreadFlock.open(null);
191             try {
192                 Thread currentThread = Thread.currentThread();
193                 assertFalse(flock1.containsThread(currentThread));
194                 assertFalse(flock2.containsThread(currentThread));
195 
196                 // start thread1 in flock1
197                 Thread thread1 = factory.newThread(awaitLatch);
198                 flock1.start(thread1);
199 
200                 // start thread2 in flock2
201                 Thread thread2 = factory.newThread(awaitLatch);
202                 flock2.start(thread2);
203 
204                 assertTrue(flock1.containsThread(thread1));
205                 assertTrue(flock1.containsThread(thread2));
206                 assertFalse(flock2.containsThread(thread1));
207                 assertTrue(flock2.containsThread(thread2));
208 
209             } finally {
210                 latch.countDown();   // release threads
211                 flock2.close();
212             }
213         }
214     }
215 
216     /**
217      * Test ThreadFlock::containsThread with a tree of flocks.
218      */
219     @ParameterizedTest
220     @MethodSource("factories")
221     void testContainsThread2(ThreadFactory factory) throws Exception {
222         CountDownLatch latch = new CountDownLatch(1);
223         var exception = new AtomicReference<Exception>();
224 
225         Runnable awaitLatch = () -> {
226             try {
227                 latch.await();
228             } catch (Exception e) {
229                 exception.compareAndSet(null, e);
230             }
231         };
232 
233         try (var flock1 = ThreadFlock.open(null)) {
234 
235             // use box to publish to enclosing scope
236             class Box {
237                 volatile ThreadFlock flock2;
238                 volatile Thread thread2;
239             }
240             var box = new Box();
241 
242             // flock1 will be "parent" of flock2
243             Thread thread1 = factory.newThread(() -> {
244                 try (var flock2 = ThreadFlock.open(null)) {
245                     Thread thread2 = factory.newThread(awaitLatch);
246                     flock2.start(thread2);
247                     box.flock2 = flock2;
248                     box.thread2 = thread2;
249                 }
250             });
251             flock1.start(thread1);
252 
253             // wait for thread2 to start
254             ThreadFlock flock2;
255             Thread thread2;
256             while ((flock2 = box.flock2) == null || (thread2 = box.thread2) == null) {
257                 Thread.sleep(20);
258             }
259 
260             try {
261                 assertTrue(flock1.containsThread(thread1));
262                 assertTrue(flock1.containsThread(thread2));
263                 assertFalse(flock2.containsThread(thread1));
264                 assertTrue(flock2.containsThread(thread2));
265             } finally {
266                 latch.countDown();   // release threads
267             }
268         }
269     }
270 
271     /**
272      * Test that start causes a thread to execute.
273      */
274     @ParameterizedTest
275     @MethodSource("factories")
276     void testStart(ThreadFactory factory) throws Exception {
277         try (var flock = ThreadFlock.open(null)) {
278             AtomicBoolean executed = new AtomicBoolean();
279             Thread thread = factory.newThread(() -> executed.set(true));
280             assertTrue(flock.start(thread) == thread);
281             thread.join();
282             assertTrue(executed.get());
283         }
284     }
285 
286     /**
287      * Test that start throws IllegalStateException when shutdown
288      */
289     @ParameterizedTest
290     @MethodSource("factories")
291     void testStartAfterShutdown(ThreadFactory factory) {
292         try (var flock = ThreadFlock.open(null)) {
293             flock.shutdown();
294             Thread thread = factory.newThread(() -> { });
295             assertThrows(IllegalStateException.class, () -> flock.start(thread));
296         }
297     }
298 
299     /**
300      * Test that start throws IllegalStateException when closed
301      */
302     @ParameterizedTest
303     @MethodSource("factories")
304     void testStartAfterClose(ThreadFactory factory) {
305         var flock = ThreadFlock.open(null);
306         flock.close();;
307         Thread thread = factory.newThread(() -> { });
308         assertThrows(IllegalStateException.class, () -> flock.start(thread));
309     }
310 
311     /**
312      * Test that start throws IllegalThreadStateException when invoked to
313      * start a thread that has already started.
314      */
315     @ParameterizedTest
316     @MethodSource("factories")
317     void testStartAfterStarted(ThreadFactory factory) {
318         try (var flock = ThreadFlock.open(null)) {
319             Thread thread = factory.newThread(() -> { });
320             flock.start(thread);
321             assertThrows(IllegalThreadStateException.class, () -> flock.start(thread));
322         }
323     }
324 
325     /**
326      * Test start is confined to threads in the flock.
327      */
328     @ParameterizedTest
329     @MethodSource("factories")
330     void testStartConfined(ThreadFactory factory) throws Exception {
331         try (var flock = ThreadFlock.open(null)) {
332             // thread in flock
333             testStartConfined(flock, task -> {
334                 Thread thread = factory.newThread(task);
335                 return flock.start(thread);
336             });
337 
338             // thread in flock
339             try (var flock2 = ThreadFlock.open(null)) {
340                 testStartConfined(flock, task -> {
341                     Thread thread = factory.newThread(task);
342                     return flock2.start(thread);
343                 });
344             }
345 
346             // thread not contained in flock
347             testStartConfined(flock, task -> {
348                 Thread thread = factory.newThread(task);
349                 thread.start();
350                 return thread;
351             });
352         }
353     }
354 
355     /**
356      * Test that a thread created with the given factory cannot start a thread
357      * in the given flock.
358      */
359     private void testStartConfined(ThreadFlock flock,
360                                    Function<Runnable, Thread> factory) throws Exception {
361         var exception = new AtomicReference<Exception>();
362         Thread thread = factory.apply(() -> {
363             try {
364                 Thread t = Thread.ofVirtual().unstarted(() -> { });
365                 flock.start(t);
366             } catch (Exception e) {
367                 exception.set(e);
368             }
369         });
370         thread.join();
371         Throwable cause = exception.get();
372         if (flock.containsThread(thread)) {
373             assertNull(cause);
374         } else {
375             assertTrue(cause instanceof WrongThreadException);
376         }
377     }
378 
379     /**
380      * Test awaitAll with no threads.
381      */
382     @Test
383     void testAwaitAllWithNoThreads() throws Exception {
384         try (var flock = ThreadFlock.open(null)) {
385             assertTrue(flock.awaitAll());
386             assertTrue(flock.awaitAll(Duration.ofSeconds(1)));
387         }
388     }
389 
390     /**
391      * Test awaitAll with threads running.
392      */
393     @ParameterizedTest
394     @MethodSource("factories")
395     void testAwaitAllWithThreads(ThreadFactory factory) throws Exception {
396         try (var flock = ThreadFlock.open(null)) {
397             AtomicBoolean done = new AtomicBoolean();
398             Runnable task = () -> {
399                 try {
400                     Thread.sleep(Duration.ofMillis(50));
401                     done.set(true);
402                 } catch (InterruptedException e) { }
403             };
404             Thread thread = factory.newThread(task);
405             flock.start(thread);
406             assertTrue(flock.awaitAll());
407             assertTrue(done.get());
408         }
409     }
410 
411     /**
412      * Test awaitAll with timeout, threads finish before timeout expires.
413      */
414     @ParameterizedTest
415     @MethodSource("factories")
416     void testAwaitAllWithTimeout1(ThreadFactory factory) throws Exception {
417         try (var flock = ThreadFlock.open(null)) {
418             Runnable task = () -> {
419                 try {
420                     Thread.sleep(Duration.ofSeconds(2));
421                 } catch (InterruptedException e) { }
422             };
423             Thread thread = factory.newThread(task);
424             flock.start(thread);
425 
426             long startMillis = millisTime();
427             boolean done = flock.awaitAll(Duration.ofSeconds(30));
428             assertTrue(done);
429             checkDuration(startMillis, 1900, 20_000);
430         }
431     }
432 
433     /**
434      * Test awaitAll with timeout, timeout expires before threads finish.
435      */
436     @ParameterizedTest
437     @MethodSource("factories")
438     void testAwaitAllWithTimeout2(ThreadFactory factory) throws Exception {
439         try (var flock = ThreadFlock.open(null)) {
440             var latch = new CountDownLatch(1);
441 
442             Runnable task = () -> {
443                 try {
444                     latch.await();
445                 } catch (InterruptedException e) { }
446             };
447             Thread thread = factory.newThread(task);
448             flock.start(thread);
449 
450             try {
451                 long startMillis = millisTime();
452                 try {
453                     flock.awaitAll(Duration.ofSeconds(2));
454                     fail("awaitAll did not throw");
455                 } catch (TimeoutException e) {
456                     checkDuration(startMillis, 1900, 20_000);
457                 }
458             } finally {
459                 latch.countDown();
460             }
461         }
462     }
463 
464     /**
465      * Test awaitAll with timeout many times.
466      */
467     @ParameterizedTest
468     @MethodSource("factories")
469     void testAwaitAllWithTimeout3(ThreadFactory factory) throws Exception {
470         try (var flock = ThreadFlock.open(null)) {
471             var latch = new CountDownLatch(1);
472 
473             Runnable task = () -> {
474                 try {
475                     latch.await();
476                 } catch (InterruptedException e) { }
477             };
478             Thread thread = factory.newThread(task);
479             flock.start(thread);
480 
481             try {
482                 for (int i = 0; i < 3; i++) {
483                     try {
484                         flock.awaitAll(Duration.ofMillis(50));
485                         fail("awaitAll did not throw");
486                     } catch (TimeoutException expected) { }
487                 }
488             } finally {
489                 latch.countDown();
490             }
491 
492             boolean done = flock.awaitAll();
493             assertTrue(done);
494         }
495     }
496 
497     /**
498      * Test awaitAll with a 0 or negative timeout.
499      */
500     @ParameterizedTest
501     @MethodSource("factories")
502     void testAwaitAllWithTimeout4(ThreadFactory factory) throws Exception {
503         try (var flock = ThreadFlock.open(null)) {
504             var latch = new CountDownLatch(1);
505 
506             Runnable task = () -> {
507                 try {
508                     latch.await();
509                 } catch (InterruptedException e) { }
510             };
511             Thread thread = factory.newThread(task);
512             flock.start(thread);
513 
514             try {
515                 try {
516                     flock.awaitAll(Duration.ofSeconds(0));
517                     fail("awaitAll did not throw");
518                 } catch (TimeoutException expected) { }
519                 try {
520                     flock.awaitAll(Duration.ofSeconds(-1));
521                     fail("awaitAll did not throw");
522                 } catch (TimeoutException expected) { }
523             } finally {
524                 latch.countDown();
525             }
526 
527             boolean done = flock.awaitAll();
528             assertTrue(done);
529         }
530     }
531 
532     /**
533      * Test awaitAll with interrupt status set, should interrupt thread.
534      */
535     @ParameterizedTest
536     @MethodSource("factories")
537     void testInterruptAwaitAll1(ThreadFactory factory) {
538         CountDownLatch latch = new CountDownLatch(1);
539         var exception = new AtomicReference<Exception>();
540         Runnable awaitLatch = () -> {
541             try {
542                 latch.await();
543             } catch (Exception e) {
544                 exception.compareAndSet(null, e);
545             }
546         };
547 
548         var flock = ThreadFlock.open(null);
549         try {
550             Thread thread = factory.newThread(awaitLatch);
551             flock.start(thread);
552 
553             // invoke awaitAll with interrupt status set.
554             Thread.currentThread().interrupt();
555             try {
556                 flock.awaitAll();
557                 fail("awaitAll did not throw");
558             } catch (InterruptedException e) {
559                 // interrupt status should be clear
560                 assertFalse(Thread.currentThread().isInterrupted());
561             }
562 
563             // invoke awaitAll(Duration) with interrupt status set.
564             Thread.currentThread().interrupt();
565             try {
566                 flock.awaitAll(Duration.ofSeconds(30));
567                 fail("awaitAll did not throw");
568             } catch (TimeoutException e) {
569                 fail("TimeoutException not expected");
570             } catch (InterruptedException e) {
571                 // interrupt status should be clear
572                 assertFalse(Thread.currentThread().isInterrupted());
573             }
574 
575         } finally {
576             Thread.interrupted();  // clear interrupt
577 
578             latch.countDown();
579             flock.close();
580         }
581 
582         // thread should not have throw any exception
583         assertNull(exception.get());
584     }
585 
586     /**
587      * Test interrupt of awaitAll.
588      */
589     @ParameterizedTest
590     @MethodSource("factories")
591     void testInterruptAwaitAll2(ThreadFactory factory) {
592         CountDownLatch latch = new CountDownLatch(1);
593         var exception = new AtomicReference<Exception>();
594         Runnable awaitLatch = () -> {
595             try {
596                 latch.await();
597             } catch (Exception e) {
598                 exception.compareAndSet(null, e);
599             }
600         };
601 
602         var flock = ThreadFlock.open(null);
603         try {
604             Thread thread = factory.newThread(awaitLatch);
605             flock.start(thread);
606 
607             scheduleInterrupt(Thread.currentThread(), Duration.ofMillis(500));
608             try {
609                 flock.awaitAll();
610                 fail("awaitAll did not throw");
611             } catch (InterruptedException e) {
612                 // interrupt status should be clear
613                 assertFalse(Thread.currentThread().isInterrupted());
614             }
615 
616             scheduleInterrupt(Thread.currentThread(), Duration.ofMillis(500));
617             try {
618                 flock.awaitAll(Duration.ofSeconds(30));
619                 fail("awaitAll did not throw");
620             } catch (TimeoutException e) {
621                 fail("TimeoutException not expected");
622             } catch (InterruptedException e) {
623                 // interrupt status should be clear
624                 assertFalse(Thread.currentThread().isInterrupted());
625             }
626 
627         } finally {
628             Thread.interrupted();  // clear interrupt
629 
630             latch.countDown();
631             flock.close();
632         }
633 
634         // thread should not have throw any exception
635         assertNull(exception.get());
636     }
637 
638     /**
639      * Test awaitAll after close.
640      */
641     @Test
642     void testAwaitAfterClose() throws Exception {
643         var flock = ThreadFlock.open(null);
644         flock.close();
645         assertTrue(flock.awaitAll());
646         assertTrue(flock.awaitAll(Duration.ofSeconds(1)));
647     }
648 
649     /**
650      * Test awaitAll is flock confined.
651      */
652     @ParameterizedTest
653     @MethodSource("factories")
654     void testAwaitAllConfined(ThreadFactory factory) throws Exception {
655         try (var flock = ThreadFlock.open(null)) {
656             // thread in flock
657             testAwaitAllConfined(flock, task -> {
658                 Thread thread = factory.newThread(task);
659                 return flock.start(thread);
660             });
661 
662             // thread not in flock
663             testAwaitAllConfined(flock, task -> {
664                 Thread thread = factory.newThread(task);
665                 thread.start();
666                 return thread;
667             });
668         }
669     }
670 
671     /**
672      * Test that a thread created with the given factory cannot call awaitAll.
673      */
674     private void testAwaitAllConfined(ThreadFlock flock,
675                                       Function<Runnable, Thread> factory) throws Exception {
676         var exception = new AtomicReference<Exception>();
677         Thread thread = factory.apply(() -> {
678             try {
679                 flock.awaitAll();
680                 flock.awaitAll(Duration.ofMillis(1));
681             } catch (Exception e) {
682                 exception.set(e);
683             }
684         });
685         thread.join();
686         Throwable cause = exception.get();
687         assertTrue(cause instanceof WrongThreadException);
688     }
689 
690     /**
691      * Test awaitAll with the wakeup permit.
692      */
693     @ParameterizedTest
694     @MethodSource("factories")
695     void testWakeupAwaitAll1(ThreadFactory factory) throws Exception {
696         try (var flock = ThreadFlock.open(null)) {
697             CountDownLatch latch = new CountDownLatch(1);
698             var exception = new AtomicReference<Exception>();
699             Runnable task = () -> {
700                 try {
701                     latch.await();
702                 } catch (Exception e) {
703                     exception.compareAndSet(null, e);
704                 }
705             };
706             Thread thread = factory.newThread(task);
707             flock.start(thread);
708 
709             // invoke awaitAll with permit
710             try {
711                 flock.wakeup();
712                 assertFalse(flock.awaitAll());
713             } finally {
714                 latch.countDown();
715             }
716         }
717     }
718 
719     /**
720      * Schedule a thread to wakeup the owner waiting in awaitAll.
721      */
722     @ParameterizedTest
723     @MethodSource("factories")
724     void testWakeupAwaitAll2(ThreadFactory factory) throws Exception {
725         try (var flock = ThreadFlock.open(null)) {
726             CountDownLatch latch = new CountDownLatch(1);
727             var exception = new AtomicReference<Exception>();
728             Runnable task = () -> {
729                 try {
730                     latch.await();
731                 } catch (Exception e) {
732                     exception.compareAndSet(null, e);
733                 }
734             };
735             Thread thread1 = factory.newThread(task);
736             flock.start(thread1);
737 
738             // schedule thread to invoke wakeup
739             Thread thread2 = factory.newThread(() -> {
740                 try { Thread.sleep(Duration.ofMillis(500)); } catch (Exception e) { }
741                 flock.wakeup();
742             });
743             flock.start(thread2);
744 
745             try {
746                 assertFalse(flock.awaitAll());
747             } finally {
748                 latch.countDown();
749             }
750         }
751     }
752 
753     /**
754      * Test close with no threads running.
755      */
756     @Test
757     void testCloseWithNoThreads() {
758         var flock = ThreadFlock.open(null);
759         flock.close();
760         assertTrue(flock.isClosed());
761         assertTrue(flock.threads().count() == 0);
762     }
763 
764     /**
765      * Test close with threads running.
766      */
767     @ParameterizedTest
768     @MethodSource("factories")
769     void testCloseWithThreads(ThreadFactory factory) {
770         var exception = new AtomicReference<Exception>();
771         Runnable sleepTask = () -> {
772             try {
773                 Thread.sleep(Duration.ofMillis(50));
774             } catch (Exception e) {
775                 exception.set(e);
776             }
777         };
778         var flock = ThreadFlock.open(null);
779         try {
780             Thread thread = factory.newThread(sleepTask);
781             flock.start(thread);
782         } finally {
783             flock.close();
784         }
785         assertTrue(flock.isClosed());
786         assertTrue(flock.threads().count() == 0);
787         assertNull(exception.get()); // no exception thrown
788     }
789 
790     /**
791      * Test close after flock is closed.
792      */
793     @Test
794     void testCloseAfterClose() {
795         var flock = ThreadFlock.open(null);
796         flock.close();
797         assertTrue(flock.isClosed());
798         flock.close();
799         assertTrue(flock.isClosed());
800     }
801 
802     /**
803      * Test close is owner confined.
804      */
805     @ParameterizedTest
806     @MethodSource("factories")
807     void testCloseConfined(ThreadFactory factory) throws Exception {
808         try (var flock = ThreadFlock.open(null)) {
809             // thread in flock
810             testCloseConfined(flock, task -> {
811                 Thread thread = factory.newThread(task);
812                 return flock.start(thread);
813             });
814 
815             // thread not in flock
816             testCloseConfined(flock, task -> {
817                 Thread thread = factory.newThread(task);
818                 thread.start();
819                 return thread;
820             });
821         }
822     }
823 
824     /**
825      * Test that a thread created with the given factory cannot close the
826      * given flock.
827      */
828     private void testCloseConfined(ThreadFlock flock,
829                                    Function<Runnable, Thread> factory) throws Exception {
830         var exception = new AtomicReference<Exception>();
831         Thread thread = factory.apply(() -> {
832             try {
833                 flock.close();
834             } catch (Exception e) {
835                 exception.set(e);
836             }
837         });
838         thread.join();
839         Throwable cause = exception.get();
840         assertTrue(cause instanceof WrongThreadException);
841     }
842 
843     /**
844      * Test close with interrupt status set, should not interrupt threads.
845      */
846     @ParameterizedTest
847     @MethodSource("factories")
848     void testInterruptClose1(ThreadFactory factory) {
849         var exception = new AtomicReference<Exception>();
850         Runnable sleepTask = () -> {
851             try {
852                 Thread.sleep(Duration.ofSeconds(1));
853             } catch (Exception e) {
854                 exception.set(e);
855             }
856         };
857         try (var flock = ThreadFlock.open(null)) {
858             Thread thread = factory.newThread(sleepTask);
859             flock.start(thread);
860             Thread.currentThread().interrupt();
861         } finally {
862             assertTrue(Thread.interrupted());  // clear interrupt
863         }
864         assertNull(exception.get());
865     }
866 
867     /**
868      * Test interrupt thread block in close.
869      */
870     @ParameterizedTest
871     @MethodSource("factories")
872     void testInterruptClose2(ThreadFactory factory) {
873         var exception = new AtomicReference<Exception>();
874         Runnable sleepTask = () -> {
875             try {
876                 Thread.sleep(Duration.ofSeconds(5));
877             } catch (Exception e) {
878                 exception.set(e);
879             }
880         };
881         try (var flock = ThreadFlock.open(null)) {
882             Thread thread = factory.newThread(sleepTask);
883             flock.start(thread);
884             scheduleInterrupt(Thread.currentThread(), Duration.ofMillis(500));
885         } finally {
886             assertTrue(Thread.interrupted());  // clear interrupt
887         }
888         assertNull(exception.get());
889     }
890 
891     /**
892      * Test that closing an enclosing thread flock closes a nested thread flocks.
893      */
894     @Test
895     void testStructureViolation() {
896         try (var flock1 = ThreadFlock.open("flock1")) {
897             try (var flock2 = ThreadFlock.open("flock2")) {
898                 try {
899                     flock1.close();
900                     fail("close did not throw");
901                 } catch (RuntimeException e) {
902                     assertTrue(e.toString().contains("Structure"));
903                 }
904                 assertTrue(flock1.isClosed());
905                 assertTrue(flock2.isClosed());
906             }
907         }
908     }
909 
910     /**
911      * Test Thread exiting with an open flock. The exiting thread should close the flock.
912      */
913     @ParameterizedTest
914     @MethodSource("factories")
915     void testThreadExitWithOpenFlock(ThreadFactory factory) throws Exception {
916         var flockRef = new AtomicReference<ThreadFlock>();
917         var childRef = new AtomicReference<Thread>();
918 
919         Thread thread = factory.newThread(() -> {
920             Thread.dumpStack();
921 
922             var flock = ThreadFlock.open(null);
923             Thread child = factory.newThread(() -> {
924                 try {
925                     Thread.sleep(Duration.ofSeconds(2));
926                 } catch (InterruptedException e) { }
927             });
928             flock.start(child);
929             flockRef.set(flock);
930             childRef.set(child);
931         });
932         thread.start();
933         thread.join();
934 
935         // flock should be closed and the child thread should have terminated
936         ThreadFlock flock = flockRef.get();
937         Thread child = childRef.get();
938         assertTrue(flock.isClosed() && child.join(Duration.ofMillis(500)));
939     }
940 
941     /**
942      * Test toString includes the flock name.
943      */
944     @Test
945     void testToString() {
946         try (var flock = ThreadFlock.open("xxxx")) {
947             assertTrue(flock.toString().contains("xxxx"));
948         }
949     }
950 
951     /**
952      * Test for NullPointerException.
953      */
954     @Test
955     void testNulls() {
956         try (var flock = ThreadFlock.open(null)) {
957             assertThrows(NullPointerException.class, () -> flock.start(null));
958             assertThrows(NullPointerException.class, () -> flock.awaitAll(null));
959             assertThrows(NullPointerException.class, () -> flock.containsThread(null));
960         }
961     }
962 
963     /**
964      * Schedules a thread to be interrupted after the given delay.
965      */
966     private void scheduleInterrupt(Thread thread, Duration delay) {
967         long millis = delay.toMillis();
968         scheduler.schedule(thread::interrupt, millis, TimeUnit.MILLISECONDS);
969     }
970 
971     /**
972      * Returns the current time in milliseconds.
973      */
974     private static long millisTime() {
975         long now = System.nanoTime();
976         return TimeUnit.MILLISECONDS.convert(now, TimeUnit.NANOSECONDS);
977     }
978 
979     /**
980      * Check the duration of a task
981      * @param start start time, in milliseconds
982      * @param min minimum expected duration, in milliseconds
983      * @param max maximum expected duration, in milliseconds
984      * @return the duration (now - start), in milliseconds
985      */
986     private static long checkDuration(long start, long min, long max) {
987         long duration = millisTime() - start;
988         assertTrue(duration >= min,
989                 "Duration " + duration + "ms, expected >= " + min + "ms");
990         assertTrue(duration <= max,
991                 "Duration " + duration + "ms, expected <= " + max + "ms");
992         return duration;
993     }
994 }