xref: /oneTBB/test/tbb/test_task_group.cpp (revision 6caecf96)
1 /*
2     Copyright (c) 2005-2021 Intel Corporation
3 
4     Licensed under the Apache License, Version 2.0 (the "License");
5     you may not use this file except in compliance with the License.
6     You may obtain a copy of the License at
7 
8         http://www.apache.org/licenses/LICENSE-2.0
9 
10     Unless required by applicable law or agreed to in writing, software
11     distributed under the License is distributed on an "AS IS" BASIS,
12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13     See the License for the specific language governing permissions and
14     limitations under the License.
15 */
16 
17 #if __TBB_CPF_BUILD
18 #define TBB_PREVIEW_ISOLATED_TASK_GROUP 1
19 #define TBB_PREVIEW_TASK_GROUP_EXTENSIONS 1
20 #endif
21 
22 #include "common/test.h"
23 #include "common/utils.h"
24 #include "oneapi/tbb/detail/_config.h"
25 #include "tbb/global_control.h"
26 
27 #include "tbb/task_group.h"
28 
29 #include "common/concurrency_tracker.h"
30 
31 #include <atomic>
32 #include <stdexcept>
33 
34 //! \file test_task_group.cpp
35 //! \brief Test for [scheduler.task_group scheduler.task_group_status] specification
36 
37 unsigned g_MaxConcurrency = 4;
38 using atomic_t = std::atomic<std::uintptr_t>;
39 unsigned MinThread = 1;
40 unsigned MaxThread = 4;
41 
42 //------------------------------------------------------------------------
43 // Tests for the thread safety of the task_group manipulations
44 //------------------------------------------------------------------------
45 
46 #include "common/spin_barrier.h"
47 
48 enum SharingMode {
49     VagabondGroup = 1,
50     ParallelWait = 2
51 };
52 
53 template<typename task_group_type>
54 class SharedGroupBodyImpl : utils::NoCopy, utils::NoAfterlife {
55     static const std::uintptr_t c_numTasks0 = 4096,
56                         c_numTasks1 = 1024;
57 
58     const std::uintptr_t m_numThreads;
59     const std::uintptr_t m_sharingMode;
60 
61     task_group_type *m_taskGroup;
62     atomic_t m_tasksSpawned,
63              m_threadsReady;
64     utils::SpinBarrier m_barrier;
65 
66     static atomic_t s_tasksExecuted;
67 
68     struct TaskFunctor {
69         SharedGroupBodyImpl *m_pOwner;
70         void operator () () const {
71             if ( m_pOwner->m_sharingMode & ParallelWait ) {
72                 while ( utils::ConcurrencyTracker::PeakParallelism() < m_pOwner->m_numThreads )
73                     utils::yield();
74             }
75             ++s_tasksExecuted;
76         }
77     };
78 
79     TaskFunctor m_taskFunctor;
80 
81     void Spawn ( std::uintptr_t numTasks ) {
82         for ( std::uintptr_t i = 0; i < numTasks; ++i ) {
83             ++m_tasksSpawned;
84             utils::ConcurrencyTracker ct;
85             m_taskGroup->run( m_taskFunctor );
86         }
87         ++m_threadsReady;
88     }
89 
90     void DeleteTaskGroup () {
91         delete m_taskGroup;
92         m_taskGroup = NULL;
93     }
94 
95     void Wait () {
96         while ( m_threadsReady != m_numThreads )
97             utils::yield();
98         const std::uintptr_t numSpawned = c_numTasks0 + c_numTasks1 * (m_numThreads - 1);
99         CHECK_MESSAGE( m_tasksSpawned == numSpawned, "Wrong number of spawned tasks. The test is broken" );
100         INFO("Max spawning parallelism is " << utils::ConcurrencyTracker::PeakParallelism() << "out of " << g_MaxConcurrency);
101         if ( m_sharingMode & ParallelWait ) {
102             m_barrier.wait( &utils::ConcurrencyTracker::Reset );
103             {
104                 utils::ConcurrencyTracker ct;
105                 m_taskGroup->wait();
106             }
107             if ( utils::ConcurrencyTracker::PeakParallelism() == 1 )
108                 WARN( "Warning: No parallel waiting detected in TestParallelWait" );
109             m_barrier.wait();
110         }
111         else
112             m_taskGroup->wait();
113         CHECK_MESSAGE( m_tasksSpawned == numSpawned, "No tasks should be spawned after wait starts. The test is broken" );
114         CHECK_MESSAGE( s_tasksExecuted == numSpawned, "Not all spawned tasks were executed" );
115     }
116 
117 public:
118     SharedGroupBodyImpl ( std::uintptr_t numThreads, std::uintptr_t sharingMode = 0 )
119         : m_numThreads(numThreads)
120         , m_sharingMode(sharingMode)
121         , m_taskGroup(NULL)
122         , m_barrier(numThreads)
123     {
124         CHECK_MESSAGE( m_numThreads > 1, "SharedGroupBody tests require concurrency" );
125         if ((m_sharingMode & VagabondGroup) && m_numThreads != 2) {
126             CHECK_MESSAGE(false, "In vagabond mode SharedGroupBody must be used with 2 threads only");
127         }
128         utils::ConcurrencyTracker::Reset();
129         s_tasksExecuted = 0;
130         m_tasksSpawned = 0;
131         m_threadsReady = 0;
132         m_taskFunctor.m_pOwner = this;
133     }
134 
135     void Run ( std::uintptr_t idx ) {
136         AssertLive();
137         if ( idx == 0 ) {
138             if (m_taskGroup || m_tasksSpawned) {
139                 CHECK_MESSAGE(false, "SharedGroupBody must be reset before reuse");
140             }
141             m_taskGroup = new task_group_type;
142             Spawn( c_numTasks0 );
143             Wait();
144             if ( m_sharingMode & VagabondGroup )
145                 m_barrier.wait();
146             else
147                 DeleteTaskGroup();
148         }
149         else {
150             while ( m_tasksSpawned == 0 )
151                 utils::yield();
152             CHECK_MESSAGE ( m_taskGroup, "Task group is not initialized");
153             Spawn (c_numTasks1);
154             if ( m_sharingMode & ParallelWait )
155                 Wait();
156             if ( m_sharingMode & VagabondGroup ) {
157                 CHECK_MESSAGE ( idx == 1, "In vagabond mode SharedGroupBody must be used with 2 threads only" );
158                 m_barrier.wait();
159                 DeleteTaskGroup();
160             }
161         }
162         AssertLive();
163     }
164 };
165 
166 template<typename task_group_type>
167 atomic_t SharedGroupBodyImpl<task_group_type>::s_tasksExecuted;
168 
169 template<typename task_group_type>
170 class  SharedGroupBody : utils::NoAssign, utils::NoAfterlife {
171     bool m_bOwner;
172     SharedGroupBodyImpl<task_group_type> *m_pImpl;
173 public:
174     SharedGroupBody ( std::uintptr_t numThreads, std::uintptr_t sharingMode = 0 )
175         : utils::NoAssign()
176         , utils::NoAfterlife()
177         , m_bOwner(true)
178         , m_pImpl( new SharedGroupBodyImpl<task_group_type>(numThreads, sharingMode) )
179     {}
180     SharedGroupBody ( const SharedGroupBody& src )
181         : utils::NoAssign()
182         , utils::NoAfterlife()
183         , m_bOwner(false)
184         , m_pImpl(src.m_pImpl)
185     {}
186     ~SharedGroupBody () {
187         if ( m_bOwner )
188             delete m_pImpl;
189     }
190     void operator() ( std::uintptr_t idx ) const {
191         // Wrap the functior into additional task group to enforce bounding.
192         task_group_type tg;
193         tg.run_and_wait([&] { m_pImpl->Run(idx); });
194     }
195 };
196 
197 template<typename task_group_type>
198 class RunAndWaitSyncronizationTestBody : utils::NoAssign {
199     utils::SpinBarrier& m_barrier;
200     std::atomic<bool>& m_completed;
201     task_group_type& m_tg;
202 public:
203     RunAndWaitSyncronizationTestBody(utils::SpinBarrier& barrier, std::atomic<bool>& completed, task_group_type& tg)
204         : m_barrier(barrier), m_completed(completed), m_tg(tg) {}
205 
206     void operator()() const {
207         m_barrier.wait();
208         utils::doDummyWork(100000);
209         m_completed = true;
210     }
211 
212     void operator()(int id) const {
213         if (id == 0) {
214             m_tg.run_and_wait(*this);
215         } else {
216             m_barrier.wait();
217             m_tg.wait();
218             CHECK_MESSAGE(m_completed, "A concurrent waiter has left the wait method earlier than work has finished");
219         }
220     }
221 };
222 
223 template<typename task_group_type>
224 void TestParallelSpawn () {
225     NativeParallelFor( g_MaxConcurrency, SharedGroupBody<task_group_type>(g_MaxConcurrency) );
226 }
227 
228 template<typename task_group_type>
229 void TestParallelWait () {
230     NativeParallelFor( g_MaxConcurrency, SharedGroupBody<task_group_type>(g_MaxConcurrency, ParallelWait) );
231 
232     utils::SpinBarrier barrier(g_MaxConcurrency);
233     std::atomic<bool> completed;
234     completed = false;
235     task_group_type tg;
236     RunAndWaitSyncronizationTestBody<task_group_type> b(barrier, completed, tg);
237     NativeParallelFor( g_MaxConcurrency, b );
238 }
239 
240 // Tests non-stack-bound task group (the group that is allocated by one thread and destroyed by the other)
241 template<typename task_group_type>
242 void TestVagabondGroup () {
243     NativeParallelFor( 2, SharedGroupBody<task_group_type>(2, VagabondGroup) );
244 }
245 
246 #include "common/memory_usage.h"
247 
248 template<typename task_group_type>
249 void TestThreadSafety() {
250     auto tests = [] {
251         for (int trail = 0; trail < 10; ++trail) {
252             TestParallelSpawn<task_group_type>();
253             TestParallelWait<task_group_type>();
254             TestVagabondGroup<task_group_type>();
255         }
256     };
257 
258     // Test and warm up allocator.
259     tests();
260 
261     // Ensure that cosumption is stabilized.
262     std::size_t initial = utils::GetMemoryUsage();
263     for (;;) {
264         tests();
265         std::size_t current = utils::GetMemoryUsage();
266         if (current <= initial) {
267             return;
268         }
269         initial = current;
270     }
271 }
272 //------------------------------------------------------------------------
273 // Common requisites of the Fibonacci tests
274 //------------------------------------------------------------------------
275 
276 const std::uintptr_t N = 20;
277 const std::uintptr_t F = 6765;
278 
279 atomic_t g_Sum;
280 
281 #define FIB_TEST_PROLOGUE() \
282     const unsigned numRepeats = g_MaxConcurrency * 4;    \
283     utils::ConcurrencyTracker::Reset()
284 
285 #define FIB_TEST_EPILOGUE(sum) \
286     CHECK(utils::ConcurrencyTracker::PeakParallelism() <= g_MaxConcurrency); \
287     CHECK( sum == numRepeats * F );
288 
289 
290 // Fibonacci tasks specified as functors
291 template<class task_group_type>
292 class FibTaskBase : utils::NoAssign, utils::NoAfterlife {
293 protected:
294     std::uintptr_t* m_pRes;
295     mutable std::uintptr_t m_Num;
296     virtual void impl() const = 0;
297 public:
298     FibTaskBase( std::uintptr_t* y, std::uintptr_t n ) : m_pRes(y), m_Num(n) {}
299     void operator()() const {
300         utils::ConcurrencyTracker ct;
301         AssertLive();
302         if( m_Num < 2 ) {
303             *m_pRes = m_Num;
304         } else {
305             impl();
306         }
307     }
308     virtual ~FibTaskBase() {}
309 };
310 
311 template<class task_group_type>
312 class FibTaskAsymmetricTreeWithFunctor : public FibTaskBase<task_group_type> {
313 public:
314     FibTaskAsymmetricTreeWithFunctor( std::uintptr_t* y, std::uintptr_t n ) : FibTaskBase<task_group_type>(y, n) {}
315     virtual void impl() const override {
316         std::uintptr_t x = ~0u;
317         task_group_type tg;
318         tg.run( FibTaskAsymmetricTreeWithFunctor(&x, this->m_Num-1) );
319         this->m_Num -= 2; tg.run_and_wait( *this );
320         *(this->m_pRes) += x;
321     }
322 };
323 
324 template<class task_group_type>
325 class FibTaskSymmetricTreeWithFunctor : public FibTaskBase<task_group_type> {
326 public:
327     FibTaskSymmetricTreeWithFunctor( std::uintptr_t* y, std::uintptr_t n ) : FibTaskBase<task_group_type>(y, n) {}
328     virtual void impl() const override {
329         std::uintptr_t x = ~0u,
330                y = ~0u;
331         task_group_type tg;
332         tg.run( FibTaskSymmetricTreeWithFunctor(&x, this->m_Num-1) );
333         tg.run( FibTaskSymmetricTreeWithFunctor(&y, this->m_Num-2) );
334         tg.wait();
335         *(this->m_pRes) = x + y;
336     }
337 };
338 
339 // Helper functions
340 template<class fib_task>
341 std::uintptr_t RunFibTask(std::uintptr_t n) {
342     std::uintptr_t res = ~0u;
343     fib_task(&res, n)();
344     return res;
345 }
346 
347 template<typename fib_task>
348 void RunFibTest() {
349     FIB_TEST_PROLOGUE();
350     std::uintptr_t sum = 0;
351     for( unsigned i = 0; i < numRepeats; ++i )
352         sum += RunFibTask<fib_task>(N);
353     FIB_TEST_EPILOGUE(sum);
354 }
355 
356 template<typename fib_task>
357 void FibFunctionNoArgs() {
358     g_Sum += RunFibTask<fib_task>(N);
359 }
360 
361 template<typename task_group_type>
362 void TestFibWithLambdas() {
363     FIB_TEST_PROLOGUE();
364     atomic_t sum;
365     sum = 0;
366     task_group_type tg;
367     for( unsigned i = 0; i < numRepeats; ++i )
368         tg.run( [&](){sum += RunFibTask<FibTaskSymmetricTreeWithFunctor<task_group_type> >(N);} );
369     tg.wait();
370     FIB_TEST_EPILOGUE(sum);
371 }
372 
373 template<typename task_group_type>
374 void TestFibWithFunctor() {
375     RunFibTest<FibTaskAsymmetricTreeWithFunctor<task_group_type> >();
376     RunFibTest< FibTaskSymmetricTreeWithFunctor<task_group_type> >();
377 }
378 
379 template<typename task_group_type>
380 void TestFibWithFunctionPtr() {
381     FIB_TEST_PROLOGUE();
382     g_Sum = 0;
383     task_group_type tg;
384     for( unsigned i = 0; i < numRepeats; ++i )
385         tg.run( &FibFunctionNoArgs<FibTaskSymmetricTreeWithFunctor<task_group_type> > );
386     tg.wait();
387     FIB_TEST_EPILOGUE(g_Sum);
388 }
389 
390 template<typename task_group_type>
391 void RunFibonacciTests() {
392     TestFibWithLambdas<task_group_type>();
393     TestFibWithFunctor<task_group_type>();
394     TestFibWithFunctionPtr<task_group_type>();
395 }
396 
397 class test_exception : public std::exception
398 {
399     const char* m_strDescription;
400 public:
401     test_exception ( const char* descr ) : m_strDescription(descr) {}
402 
403     const char* what() const throw() override { return m_strDescription; }
404 };
405 
406 #if TBB_USE_CAPTURED_EXCEPTION
407     #include "tbb/tbb_exception.h"
408     typedef tbb::captured_exception TestException;
409 #else
410     typedef test_exception TestException;
411 #endif
412 
413 #include <string.h>
414 
415 #define NUM_CHORES      512
416 #define NUM_GROUPS      64
417 #define SKIP_CHORES     (NUM_CHORES/4)
418 #define SKIP_GROUPS     (NUM_GROUPS/4)
419 #define EXCEPTION_DESCR1 "Test exception 1"
420 #define EXCEPTION_DESCR2 "Test exception 2"
421 
422 atomic_t g_ExceptionCount;
423 atomic_t g_TaskCount;
424 unsigned g_ExecutedAtCancellation;
425 bool g_Rethrow;
426 bool g_Throw;
427 
428 class ThrowingTask : utils::NoAssign, utils::NoAfterlife {
429     atomic_t &m_TaskCount;
430 public:
431     ThrowingTask( atomic_t& counter ) : m_TaskCount(counter) {}
432     void operator() () const {
433         utils::ConcurrencyTracker ct;
434         AssertLive();
435         if ( g_Throw ) {
436             if ( ++m_TaskCount == SKIP_CHORES )
437                 TBB_TEST_THROW(test_exception(EXCEPTION_DESCR1));
438             utils::yield();
439         }
440         else {
441             ++g_TaskCount;
442             while( !tbb::is_current_task_group_canceling() )
443                 utils::yield();
444         }
445     }
446 };
447 
448 inline void ResetGlobals ( bool bThrow, bool bRethrow ) {
449     g_Throw = bThrow;
450     g_Rethrow = bRethrow;
451     g_ExceptionCount = 0;
452     g_TaskCount = 0;
453     utils::ConcurrencyTracker::Reset();
454 }
455 
456 template<typename task_group_type>
457 void LaunchChildrenWithFunctor () {
458     atomic_t count;
459     count = 0;
460     task_group_type g;
461     for (unsigned i = 0; i < NUM_CHORES; ++i) {
462 #if __TBB_PREVIEW_TASK_GROUP_EXTENSIONS
463         if (i % 2 == 1) {
464             g.run(g.defer(ThrowingTask(count)));
465         } else
466 #endif
467         {
468             g.run(ThrowingTask(count));
469         }
470     }
471 #if TBB_USE_EXCEPTIONS
472     tbb::task_group_status status = tbb::not_complete;
473     bool exceptionCaught = false;
474     try {
475         status = g.wait();
476     } catch ( TestException& e ) {
477         CHECK_MESSAGE( e.what(), "Empty what() string" );
478         CHECK_MESSAGE( strcmp(e.what(), EXCEPTION_DESCR1) == 0, "Unknown exception" );
479         exceptionCaught = true;
480         ++g_ExceptionCount;
481     } catch( ... ) { CHECK_MESSAGE( false, "Unknown exception" ); }
482     if (g_Throw && !exceptionCaught && status != tbb::canceled) {
483         CHECK_MESSAGE(false, "No exception in the child task group");
484     }
485     if ( g_Rethrow && g_ExceptionCount > SKIP_GROUPS ) {
486         throw test_exception(EXCEPTION_DESCR2);
487     }
488 #else
489     g.wait();
490 #endif
491 }
492 
493 // Tests for cancellation and exception handling behavior
494 template<typename task_group_type>
495 void TestManualCancellationWithFunctor () {
496     ResetGlobals( false, false );
497     task_group_type tg;
498     for (unsigned i = 0; i < NUM_GROUPS; ++i) {
499         // TBB version does not require taking function address
500 #if __TBB_PREVIEW_TASK_GROUP_EXTENSIONS
501         if (i % 2 == 0) {
502             auto h = tg.defer(&LaunchChildrenWithFunctor<task_group_type>);
503             tg.run(std::move(h));
504         } else
505 #endif
506         {
507             tg.run(&LaunchChildrenWithFunctor<task_group_type>);
508         }
509     }
510     CHECK_MESSAGE ( !tbb::is_current_task_group_canceling(), "Unexpected cancellation" );
511     while ( g_MaxConcurrency > 1 && g_TaskCount == 0 )
512         utils::yield();
513     tg.cancel();
514     g_ExecutedAtCancellation = int(g_TaskCount);
515     tbb::task_group_status status = tg.wait();
516     CHECK_MESSAGE( status == tbb::canceled, "Task group reported invalid status." );
517     CHECK_MESSAGE( g_TaskCount <= NUM_GROUPS * NUM_CHORES, "Too many tasks reported. The test is broken" );
518     CHECK_MESSAGE( g_TaskCount < NUM_GROUPS * NUM_CHORES, "No tasks were cancelled. Cancellation model changed?" );
519     CHECK_MESSAGE( g_TaskCount <= g_ExecutedAtCancellation + utils::ConcurrencyTracker::PeakParallelism(), "Too many tasks survived cancellation" );
520 }
521 
522 #if TBB_USE_EXCEPTIONS
523 template<typename task_group_type>
524 void TestExceptionHandling1 () {
525     ResetGlobals( true, false );
526     task_group_type tg;
527     for( unsigned i = 0; i < NUM_GROUPS; ++i )
528         // TBB version does not require taking function address
529         tg.run( &LaunchChildrenWithFunctor<task_group_type> );
530     try {
531         tg.wait();
532     } catch ( ... ) {
533         CHECK_MESSAGE( false, "Unexpected exception" );
534     }
535     CHECK_MESSAGE( g_ExceptionCount <= NUM_GROUPS, "Too many exceptions from the child groups. The test is broken" );
536     CHECK_MESSAGE( g_ExceptionCount == NUM_GROUPS, "Not all child groups threw the exception" );
537 }
538 
539 template<typename task_group_type>
540 void TestExceptionHandling2 () {
541     ResetGlobals( true, true );
542     task_group_type tg;
543     bool exceptionCaught = false;
544     for( unsigned i = 0; i < NUM_GROUPS; ++i ) {
545         // TBB version does not require taking function address
546         tg.run( &LaunchChildrenWithFunctor<task_group_type> );
547     }
548     try {
549         tg.wait();
550     } catch ( TestException& e ) {
551         CHECK_MESSAGE( e.what(), "Empty what() string" );
552         CHECK_MESSAGE( strcmp(e.what(), EXCEPTION_DESCR2) == 0, "Unknown exception" );
553         exceptionCaught = true;
554     } catch( ... ) { CHECK_MESSAGE( false, "Unknown exception" ); }
555     CHECK_MESSAGE( exceptionCaught, "No exception thrown from the root task group" );
556     CHECK_MESSAGE( g_ExceptionCount >= SKIP_GROUPS, "Too few exceptions from the child groups. The test is broken" );
557     CHECK_MESSAGE( g_ExceptionCount <= NUM_GROUPS - SKIP_GROUPS, "Too many exceptions from the child groups. The test is broken" );
558     CHECK_MESSAGE( g_ExceptionCount < NUM_GROUPS - SKIP_GROUPS, "None of the child groups was cancelled" );
559 }
560 
561 template <typename task_group_type>
562 void TestExceptionHandling3() {
563     task_group_type tg;
564     try {
565         tg.run_and_wait([]() {
566             volatile bool suppress_unreachable_code_warning = true;
567             if (suppress_unreachable_code_warning) {
568                 throw 1;
569             }
570         });
571     } catch (int error) {
572         CHECK(error == 1);
573     } catch ( ... ) {
574         CHECK_MESSAGE( false, "Unexpected exception" );
575     }
576 }
577 
578 template<typename task_group_type>
579 class LaunchChildrenDriver {
580 public:
581     void Launch(task_group_type& tg) {
582         ResetGlobals(false, false);
583         for (unsigned i = 0; i < NUM_GROUPS; ++i) {
584             tg.run(LaunchChildrenWithFunctor<task_group_type>);
585         }
586         CHECK_MESSAGE(!tbb::is_current_task_group_canceling(), "Unexpected cancellation");
587         while (g_MaxConcurrency > 1 && g_TaskCount == 0)
588             utils::yield();
589     }
590 
591     void Finish() {
592         CHECK_MESSAGE(g_TaskCount <= NUM_GROUPS * NUM_CHORES, "Too many tasks reported. The test is broken");
593         CHECK_MESSAGE(g_TaskCount < NUM_GROUPS * NUM_CHORES, "No tasks were cancelled. Cancellation model changed?");
594         CHECK_MESSAGE(g_TaskCount <= g_ExecutedAtCancellation + g_MaxConcurrency, "Too many tasks survived cancellation");
595     }
596 }; // LaunchChildrenWithTaskHandleDriver
597 
598 template<typename task_group_type, bool Throw>
599 void TestMissingWait () {
600     bool exception_occurred = false,
601          unexpected_exception = false;
602     LaunchChildrenDriver<task_group_type> driver;
603     try {
604         task_group_type tg;
605         driver.Launch( tg );
606         volatile bool suppress_unreachable_code_warning = Throw;
607         if (suppress_unreachable_code_warning) {
608             throw int(); // Initiate stack unwinding
609         }
610     }
611     catch ( const tbb::missing_wait& e ) {
612         CHECK_MESSAGE( e.what(), "Error message is absent" );
613         exception_occurred = true;
614         unexpected_exception = Throw;
615     }
616     catch ( int ) {
617         exception_occurred = true;
618         unexpected_exception = !Throw;
619     }
620     catch ( ... ) {
621         exception_occurred = unexpected_exception = true;
622     }
623     CHECK( exception_occurred );
624     CHECK( !unexpected_exception );
625     driver.Finish();
626 }
627 #endif
628 
629 template<typename task_group_type>
630 void RunCancellationAndExceptionHandlingTests() {
631     TestManualCancellationWithFunctor<task_group_type>();
632 #if TBB_USE_EXCEPTIONS
633     TestExceptionHandling1<task_group_type>();
634     TestExceptionHandling2<task_group_type>();
635     TestExceptionHandling3<task_group_type>();
636     TestMissingWait<task_group_type, true>();
637     TestMissingWait<task_group_type, false>();
638 #endif
639 }
640 
641 void EmptyFunction () {}
642 
643 struct TestFunctor {
644     void operator()() { CHECK_MESSAGE( false, "Non-const operator called" ); }
645     void operator()() const { /* library requires this overload only */ }
646 };
647 
648 template<typename task_group_type>
649 void TestConstantFunctorRequirement() {
650     task_group_type g;
651     TestFunctor tf;
652     g.run( tf ); g.wait();
653     g.run_and_wait( tf );
654 }
655 
656 //------------------------------------------------------------------------
657 namespace TestMoveSemanticsNS {
658     struct TestFunctor {
659         void operator()() const {};
660     };
661 
662     struct MoveOnlyFunctor : utils::MoveOnly, TestFunctor {
663         MoveOnlyFunctor() : utils::MoveOnly() {};
664         MoveOnlyFunctor(MoveOnlyFunctor&& other) : utils::MoveOnly(std::move(other)) {};
665     };
666 
667     struct MovePreferableFunctor : utils::Movable, TestFunctor {
668         MovePreferableFunctor() : utils::Movable() {};
669         MovePreferableFunctor(MovePreferableFunctor&& other) : utils::Movable(std::move(other)) {};
670         MovePreferableFunctor(const MovePreferableFunctor& other) : utils::Movable(other) {};
671     };
672 
673     struct NoMoveNoCopyFunctor : utils::NoCopy, TestFunctor {
674         NoMoveNoCopyFunctor() : utils::NoCopy() {};
675         // mv ctor is not allowed as cp ctor from parent utils::NoCopy
676     private:
677         NoMoveNoCopyFunctor(NoMoveNoCopyFunctor&&);
678     };
679 
680      template<typename task_group_type>
681     void TestBareFunctors() {
682         task_group_type tg;
683         MovePreferableFunctor mpf;
684         // run_and_wait() doesn't have any copies or moves of arguments inside the impl
685         tg.run_and_wait( NoMoveNoCopyFunctor() );
686 
687         tg.run( MoveOnlyFunctor() );
688         tg.wait();
689 
690         tg.run( mpf );
691         tg.wait();
692         CHECK_MESSAGE(mpf.alive, "object was moved when was passed by lval");
693         mpf.Reset();
694 
695         tg.run( std::move(mpf) );
696         tg.wait();
697         CHECK_MESSAGE(!mpf.alive, "object was copied when was passed by rval");
698         mpf.Reset();
699     }
700 }
701 
702 template<typename task_group_type>
703 void TestMoveSemantics() {
704     TestMoveSemanticsNS::TestBareFunctors<task_group_type>();
705 }
706 //------------------------------------------------------------------------
707 
708 // TODO: TBB_REVAMP_TODO - enable when ETS is available
709 #if TBBTEST_USE_TBB && TBB_PREVIEW_ISOLATED_TASK_GROUP
710 namespace TestIsolationNS {
711     class DummyFunctor {
712     public:
713         DummyFunctor() {}
714         void operator()() const {
715             for ( volatile int j = 0; j < 10; ++j ) {}
716         }
717     };
718 
719     template<typename task_group_type>
720     class ParForBody {
721         task_group_type& m_tg;
722         std::atomic<bool>& m_preserved;
723         tbb::enumerable_thread_specific<int>& m_ets;
724     public:
725         ParForBody(
726             task_group_type& tg,
727             std::atomic<bool>& preserved,
728             tbb::enumerable_thread_specific<int>& ets
729         ) : m_tg(tg), m_preserved(preserved), m_ets(ets) {}
730 
731         void operator()(int) const {
732             if (++m_ets.local() > 1) m_preserved = false;
733 
734             for (int i = 0; i < 1000; ++i)
735                 m_tg.run(DummyFunctor());
736             m_tg.wait();
737             m_tg.run_and_wait(DummyFunctor());
738 
739             --m_ets.local();
740         }
741     };
742 
743     template<typename task_group_type>
744     void CheckIsolation(bool isolation_is_expected) {
745         task_group_type tg;
746         std::atomic<bool> isolation_is_preserved;
747         isolation_is_preserved = true;
748         tbb::enumerable_thread_specific<int> ets(0);
749 
750         tbb::parallel_for(0, 100, ParForBody<task_group_type>(tg, isolation_is_preserved, ets));
751 
752         ASSERT(
753             isolation_is_expected == isolation_is_preserved,
754             "Actual and expected isolation-related behaviours are different"
755         );
756     }
757 
758     // Should be called only when > 1 thread is used, because otherwise isolation is guaranteed to take place
759     void TestIsolation() {
760         CheckIsolation<tbb::task_group>(false);
761         CheckIsolation<tbb::isolated_task_group>(true);
762     }
763 }
764 #endif
765 
766 //! Test for thread safety for the task_group
767 //! \brief \ref error_guessing \ref resource_usage
768 TEST_CASE("Thread safety test for the task group") {
769     if (tbb::this_task_arena::max_concurrency() < 2) {
770         // The test requires more than one thread to check thread safety
771         return;
772     }
773     for (unsigned p=MinThread; p <= MaxThread; ++p) {
774         if (p < 2) {
775             continue;
776         }
777         tbb::global_control limit(tbb::global_control::max_allowed_parallelism, p);
778         g_MaxConcurrency = p;
779         TestThreadSafety<tbb::task_group>();
780     }
781 }
782 
783 //! Fibonacci test for task group
784 //! \brief \ref interface \ref requirement
785 TEST_CASE("Fibonacci test for the task group") {
786     for (unsigned p=MinThread; p <= MaxThread; ++p) {
787         tbb::global_control limit(tbb::global_control::max_allowed_parallelism, p);
788         g_MaxConcurrency = p;
789         RunFibonacciTests<tbb::task_group>();
790     }
791 }
792 
793 //! Cancellation and exception test for the task group
794 //! \brief \ref interface \ref requirement
795 TEST_CASE("Cancellation and exception test for the task group") {
796     for (unsigned p = MinThread; p <= MaxThread; ++p) {
797         tbb::global_control limit(tbb::global_control::max_allowed_parallelism, p);
798         tbb::task_arena a(p);
799         g_MaxConcurrency = p;
800         a.execute([] {
801             RunCancellationAndExceptionHandlingTests<tbb::task_group>();
802         });
803     }
804 }
805 
806 //! Constant functor test for the task group
807 //! \brief \ref interface \ref negative
808 TEST_CASE("Constant functor test for the task group") {
809     for (unsigned p=MinThread; p <= MaxThread; ++p) {
810         tbb::global_control limit(tbb::global_control::max_allowed_parallelism, p);
811         g_MaxConcurrency = p;
812         TestConstantFunctorRequirement<tbb::task_group>();
813     }
814 }
815 
816 //! Move semantics test for the task group
817 //! \brief \ref interface \ref requirement
818 TEST_CASE("Move semantics test for the task group") {
819     for (unsigned p=MinThread; p <= MaxThread; ++p) {
820         tbb::global_control limit(tbb::global_control::max_allowed_parallelism, p);
821         g_MaxConcurrency = p;
822         TestMoveSemantics<tbb::task_group>();
823     }
824 }
825 
826 #if TBB_PREVIEW_ISOLATED_TASK_GROUP
827 //! Test for thread safety for the isolated_task_group
828 //! \brief \ref error_guessing
829 TEST_CASE("Thread safety test for the isolated task group") {
830     if (tbb::this_task_arena::max_concurrency() < 2) {
831         // The test requires more than one thread to check thread safety
832         return;
833     }
834     for (unsigned p=MinThread; p <= MaxThread; ++p) {
835         if (p < 2) {
836             continue;
837         }
838         tbb::global_control limit(tbb::global_control::max_allowed_parallelism, p);
839         g_MaxConcurrency = p;
840         TestThreadSafety<tbb::isolated_task_group>();
841     }
842 }
843 
844 //! Cancellation and exception test for the isolated task group
845 //! \brief \ref interface \ref requirement
846 TEST_CASE("Fibonacci test for the isolated task group") {
847     for (unsigned p=MinThread; p <= MaxThread; ++p) {
848         tbb::global_control limit(tbb::global_control::max_allowed_parallelism, p);
849         g_MaxConcurrency = p;
850         RunFibonacciTests<tbb::isolated_task_group>();
851     }
852 }
853 
854 //! Cancellation and exception test for the isolated task group
855 //! \brief \ref interface \ref requirement
856 TEST_CASE("Cancellation and exception test for the isolated task group") {
857     for (unsigned p=MinThread; p <= MaxThread; ++p) {
858         tbb::global_control limit(tbb::global_control::max_allowed_parallelism, p);
859         g_MaxConcurrency = p;
860         RunCancellationAndExceptionHandlingTests<tbb::isolated_task_group>();
861     }
862 }
863 
864 //! Constant functor test for the isolated task group.
865 //! \brief \ref interface \ref negative
866 TEST_CASE("Constant functor test for the isolated task group") {
867     for (unsigned p=MinThread; p <= MaxThread; ++p) {
868         tbb::global_control limit(tbb::global_control::max_allowed_parallelism, p);
869         g_MaxConcurrency = p;
870         TestConstantFunctorRequirement<tbb::isolated_task_group>();
871     }
872 }
873 
874 //! Move semantics test for the isolated task group.
875 //! \brief \ref interface \ref requirement
876 TEST_CASE("Move semantics test for the isolated task group") {
877     for (unsigned p=MinThread; p <= MaxThread; ++p) {
878         tbb::global_control limit(tbb::global_control::max_allowed_parallelism, p);
879         g_MaxConcurrency = p;
880         TestMoveSemantics<tbb::isolated_task_group>();
881     }
882 }
883 #endif /* TBB_PREVIEW_ISOLATED_TASK_GROUP */
884 
885 void run_deep_stealing(tbb::task_group& tg1, tbb::task_group& tg2, int num_tasks, std::atomic<int>& tasks_executed) {
886     for (int i = 0; i < num_tasks; ++i) {
887         tg2.run([&tg1, &tasks_executed] {
888             volatile char consume_stack[1000]{};
889             ++tasks_executed;
890             tg1.wait();
891             utils::suppress_unused_warning(consume_stack);
892         });
893     }
894 }
895 
896 // TODO: move to the conformance test
897 //! Test for stack overflow avoidance mechanism.
898 //! \brief \ref requirement
899 TEST_CASE("Test for stack overflow avoidance mechanism") {
900     if (tbb::this_task_arena::max_concurrency() < 2) {
901         return;
902     }
903 
904     tbb::global_control thread_limit(tbb::global_control::max_allowed_parallelism, 2);
905     tbb::task_group tg1;
906     tbb::task_group tg2;
907     std::atomic<int> tasks_executed{};
908     tg1.run_and_wait([&tg1, &tg2, &tasks_executed] {
909         run_deep_stealing(tg1, tg2, 10000, tasks_executed);
910         while (tasks_executed < 100) {
911             // Some stealing is expected to happen.
912             utils::yield();
913         }
914         CHECK(tasks_executed < 10000);
915     });
916     tg2.wait();
917     CHECK(tasks_executed == 10000);
918 }
919 
920 //! Test for stack overflow avoidance mechanism.
921 //! \brief \ref error_guessing
922 TEST_CASE("Test for stack overflow avoidance mechanism within arena") {
923     if (tbb::this_task_arena::max_concurrency() < 2) {
924         return;
925     }
926 
927     tbb::global_control thread_limit(tbb::global_control::max_allowed_parallelism, 2);
928     tbb::task_group tg1;
929     tbb::task_group tg2;
930     std::atomic<int> tasks_executed{};
931 
932     // Determine nested task execution limit.
933     int second_thread_executed{};
934     tg1.run_and_wait([&tg1, &tg2, &tasks_executed, &second_thread_executed] {
935         run_deep_stealing(tg1, tg2, 10000, tasks_executed);
936         do {
937             second_thread_executed = tasks_executed;
938             utils::Sleep(10);
939         } while (second_thread_executed < 100 || second_thread_executed != tasks_executed);
940         CHECK(tasks_executed < 10000);
941     });
942     tg2.wait();
943     CHECK(tasks_executed == 10000);
944 
945     tasks_executed = 0;
946     tbb::task_arena a(2, 2);
947     tg1.run_and_wait([&a, &tg1, &tg2, &tasks_executed, second_thread_executed] {
948         run_deep_stealing(tg1, tg2, second_thread_executed-1, tasks_executed);
949         while (tasks_executed < second_thread_executed-1) {
950             // Wait until the second thread near the limit.
951             utils::yield();
952         }
953         tg2.run([&a, &tg1, &tasks_executed] {
954             a.execute([&tg1, &tasks_executed] {
955                 volatile char consume_stack[1000]{};
956                 ++tasks_executed;
957                 tg1.wait();
958                 utils::suppress_unused_warning(consume_stack);
959             });
960         });
961         while (tasks_executed < second_thread_executed) {
962             // Wait until the second joins the arena.
963             utils::yield();
964         }
965         a.execute([&tg1, &tg2, &tasks_executed] {
966             run_deep_stealing(tg1, tg2, 10000, tasks_executed);
967         });
968         int currently_executed{};
969         do {
970             currently_executed = tasks_executed;
971             utils::Sleep(10);
972         } while (currently_executed != tasks_executed);
973         CHECK(tasks_executed < 10000 + second_thread_executed);
974     });
975     a.execute([&tg2] {
976         tg2.wait();
977     });
978     CHECK(tasks_executed == 10000 + second_thread_executed);
979 }
980 
981 //! Test checks that we can submit work to task_group asynchronously with waiting.
982 //! \brief \ref regression
983 TEST_CASE("Async task group") {
984     int num_threads = tbb::this_task_arena::max_concurrency();
985     if (num_threads < 3) {
986         // The test requires at least 2 worker threads
987         return;
988     }
989     tbb::task_arena a(2*num_threads, num_threads);
990     utils::SpinBarrier barrier(num_threads + 2);
991     tbb::task_group tg[2];
992     std::atomic<bool> finished[2]{};
993     finished[0] = false; finished[1] = false;
994     for (int i = 0; i < 2; ++i) {
995         a.enqueue([i, &tg, &finished, &barrier] {
996             barrier.wait();
997             for (int j = 0; j < 10000; ++j) {
998                 tg[i].run([] {});
999                 utils::yield();
1000             }
1001             finished[i] = true;
1002         });
1003     }
1004     utils::NativeParallelFor(num_threads, [&](int idx) {
1005         barrier.wait();
1006         a.execute([idx, &tg, &finished] {
1007             std::size_t counter{};
1008             while (!finished[idx%2]) {
1009                 tg[idx%2].wait();
1010                 if (counter++ % 16 == 0) utils::yield();
1011             }
1012             tg[idx%2].wait();
1013         });
1014     });
1015 }
1016 
1017 struct SelfRunner {
1018     tbb::task_group& m_tg;
1019     std::atomic<unsigned>& count;
1020     void operator()() const {
1021         unsigned previous_count = count.fetch_sub(1);
1022         if (previous_count > 1)
1023             m_tg.run( *this );
1024     }
1025 };
1026 
1027 //! Submit work to single task_group instance from inside the work
1028 //! \brief \ref error_guessing
1029 TEST_CASE("Run self using same task_group instance") {
1030     const unsigned num = 10;
1031     std::atomic<unsigned> count{num};
1032     tbb::task_group tg;
1033     SelfRunner uf{tg, count};
1034     tg.run( uf );
1035     tg.wait();
1036     CHECK_MESSAGE(
1037         count == 0,
1038         "Not all tasks were spawned from inside the functor running within task_group."
1039     );
1040 }
1041 
1042 #if TBB_PREVIEW_TASK_GROUP_EXTENSIONS
1043 
1044 namespace accept_task_group_context {
1045 
1046 template <typename TaskGroup, typename CancelF, typename WaitF>
1047 void run_cancellation_use_case(CancelF&& cancel, WaitF&& wait) {
1048     std::atomic<bool> outer_cancelled{false};
1049     std::atomic<unsigned> count{13};
1050 
1051     tbb::task_group_context inner_ctx(tbb::task_group_context::isolated);
1052     TaskGroup inner_tg(inner_ctx);
1053 
1054     tbb::task_group outer_tg;
1055     auto outer_tg_task = [&] {
1056         inner_tg.run([&] {
1057             utils::SpinWaitUntilEq(outer_cancelled, true);
1058             inner_tg.run( SelfRunner{inner_tg, count} );
1059         });
1060 
1061         utils::try_call([&] {
1062             std::forward<CancelF>(cancel)(outer_tg);
1063         }).on_completion([&] {
1064             outer_cancelled = true;
1065         });
1066     };
1067 
1068     auto check = [&] {
1069         tbb::task_group_status outer_status = tbb::task_group_status::not_complete;
1070         outer_status = std::forward<WaitF>(wait)(outer_tg);
1071         CHECK_MESSAGE(
1072             outer_status == tbb::task_group_status::canceled,
1073             "Outer task group should have been cancelled."
1074         );
1075 
1076         tbb::task_group_status inner_status = inner_tg.wait();
1077         CHECK_MESSAGE(
1078             inner_status == tbb::task_group_status::complete,
1079             "Inner task group should have completed despite the cancellation of the outer one."
1080         );
1081 
1082         CHECK_MESSAGE(0 == count, "Some of the inner group tasks were not executed.");
1083     };
1084 
1085     outer_tg.run(outer_tg_task);
1086     check();
1087 }
1088 
1089 template <typename TaskGroup>
1090 void test() {
1091     run_cancellation_use_case<TaskGroup>(
1092         [](tbb::task_group& outer) { outer.cancel(); },
1093         [](tbb::task_group& outer) { return outer.wait(); }
1094     );
1095 
1096 #if TBB_USE_EXCEPTIONS
1097     run_cancellation_use_case<TaskGroup>(
1098         [](tbb::task_group& /*outer*/) {
1099             volatile bool suppress_unreachable_code_warning = true;
1100             if (suppress_unreachable_code_warning) {
1101                 throw int();
1102             }
1103         },
1104         [](tbb::task_group& outer) {
1105             try {
1106                 outer.wait();
1107                 return tbb::task_group_status::complete;
1108             } catch(const int&) {
1109                 return tbb::task_group_status::canceled;
1110             }
1111         }
1112     );
1113 #endif
1114 }
1115 
1116 } // namespace accept_task_group_context
1117 
1118 //! Respect task_group_context passed from outside
1119 //! \brief \ref interface \ref requirement
1120 TEST_CASE("Respect task_group_context passed from outside") {
1121     accept_task_group_context::test<tbb::task_group>();
1122 #if TBB_PREVIEW_ISOLATED_TASK_GROUP
1123     accept_task_group_context::test<tbb::isolated_task_group>();
1124 #endif
1125 }
1126 #endif // TBB_PREVIEW_TASK_GROUP_EXTENSIONS
1127 
1128 #if __TBB_PREVIEW_TASK_GROUP_EXTENSIONS
1129 //! Test checks that for lost task handle
1130 //! \brief \ref requirement
1131 TEST_CASE("Task handle created but not run"){
1132     {
1133         tbb::task_group tg;
1134 
1135         std::atomic<bool> run {false};
1136 
1137         auto h = tg.defer([&]{
1138             run = true;
1139         });
1140         CHECK_MESSAGE(run == false, "delayed task should not be run until run(task_handle) is called");
1141     }
1142 }
1143 
1144 //! Basic test for task handle
1145 //! \brief \ref interface \ref requirement
1146 TEST_CASE("Task handle run"){
1147     tbb::task_handle h;
1148 
1149     tbb::task_group tg;
1150     std::atomic<bool> run {false};
1151 
1152     h = tg.defer([&]{
1153         run = true;
1154     });
1155     CHECK_MESSAGE(run == false, "delayed task should not be run until run(task_handle) is called");
1156     tg.run(std::move(h));
1157     tg.wait();
1158     CHECK_MESSAGE(run == true, "Delayed task should be completed when task_group::wait exits");
1159 
1160     CHECK_MESSAGE(h == nullptr, "Delayed task can be executed only once");
1161 }
1162 
1163 //! Test for empty check
1164 //! \brief \ref interface
1165 TEST_CASE("Task handle empty check"){
1166     tbb::task_group tg;
1167 
1168     tbb::task_handle h;
1169 
1170     bool empty = (h == nullptr);
1171     CHECK_MESSAGE(empty, "default constructed task_handle should be empty");
1172 
1173     h = tg.defer([]{});
1174 
1175     CHECK_MESSAGE(h != nullptr, "delayed task returned by task_group::delayed should not be empty");
1176 }
1177 
1178 //! Test that task_handle prolongs task_group::wait
1179 //! \brief \ref requirement
1180 TEST_CASE("Task handle blocks wait"){
1181     tbb::task_group tg;
1182 
1183     std::atomic<bool> completed  {false};
1184     std::atomic<bool> start_wait {false};
1185     std::atomic<bool> thread_started{false};
1186 
1187     tbb::task_handle h = tg.defer([&]{
1188         completed = true;
1189     });
1190 
1191     std::thread wait_thread {[&]{
1192         CHECK_MESSAGE(completed == false, "Deferred task should not be run until run(task_handle) is called");
1193 
1194         thread_started = true;
1195         utils::SpinWaitUntilEq(start_wait, true);
1196         tg.wait();
1197         CHECK_MESSAGE(completed == true, "Deferred task should be completed when task_group::wait exits");
1198     }};
1199 
1200     utils::SpinWaitUntilEq(thread_started, true);
1201     CHECK_MESSAGE(completed == false, "Deferred task should not be run until run(task_handle) is called");
1202 
1203     tg.run(std::move(h));
1204     //TODO: more accurate test (with fixed number of threads (1 ?) to guarantee correctness of following assert)
1205     //CHECK_MESSAGE(completed == false, "Deferred task should not be run until run(task_handle) and wait is called");
1206     start_wait = true;
1207     wait_thread.join();
1208 }
1209 
1210 //! The test for task_handle inside other task waiting with run
1211 //! \brief \ref requirement
1212 TEST_CASE("Task handle for scheduler bypass"){
1213     tbb::task_group tg;
1214     std::atomic<bool> run {false};
1215 
1216     tg.run([&]{
1217         return tg.defer([&]{
1218             run = true;
1219         });
1220     });
1221 
1222     tg.wait();
1223     CHECK_MESSAGE(run == true, "task handle returned by user lambda (bypassed) should be run");
1224 }
1225 
1226 //! The test for task_handle inside other task waiting with run_and_wait
1227 //! \brief \ref requirement
1228 TEST_CASE("Task handle for scheduler bypass via run_and_wait"){
1229     tbb::task_group tg;
1230     std::atomic<bool> run {false};
1231 
1232     tg.run_and_wait([&]{
1233         return tg.defer([&]{
1234             run = true;
1235         });
1236     });
1237 
1238     CHECK_MESSAGE(run == true, "task handle returned by user lambda (bypassed) should be run");
1239 }
1240 
1241 #if TBB_USE_EXCEPTIONS
1242 //! The test for exception handling in task_handle
1243 //! \brief \ref requirement
1244 TEST_CASE("Task handle exception propagation"){
1245     tbb::task_group tg;
1246 
1247     tbb::task_handle h = tg.defer([&]{
1248         volatile bool suppress_unreachable_code_warning = true;
1249         if (suppress_unreachable_code_warning) {
1250             throw std::runtime_error{ "" };
1251         }
1252     });
1253 
1254     tg.run(std::move(h));
1255 
1256     CHECK_THROWS_AS(tg.wait(), std::runtime_error);
1257 }
1258 
1259 //! The test for error in scheduling empty task_handle
1260 //! \brief \ref requirement
1261 TEST_CASE("Empty task_handle cannot be scheduled"){
1262     tbb::task_group tg;
1263 
1264     CHECK_THROWS_WITH_AS(tg.run(tbb::task_handle{}), "Attempt to schedule empty task_handle", std::runtime_error);
1265 }
1266 
1267 //! The test for error in task_handle being scheduled into task_group different from one it was created from
1268 //! \brief \ref requirement
1269 TEST_CASE("task_handle cannot be scheduled into different task_group"){
1270     tbb::task_group tg;
1271     tbb::task_group tg1;
1272 
1273     CHECK_THROWS_WITH_AS(tg1.run(tg.defer([]{})), "Attempt to schedule task_handle into different task_group", std::runtime_error);
1274 }
1275 
1276 //! The test for error in task_handle being scheduled into task_group different from one it was created from
1277 //! \brief \ref requirement
1278 TEST_CASE("task_handle cannot be scheduled into other task_group of the same context"
1279         * doctest::should_fail()    //Implementation is no there yet, as it is not clear that is the expected behaviour
1280         * doctest::skip()           //skip the test for now, to not pollute the test log
1281 )
1282 {
1283     tbb::task_group_context ctx;
1284 
1285     tbb::task_group tg(ctx);
1286     tbb::task_group tg1(ctx);
1287 
1288     CHECK_NOTHROW(tg.run(tg.defer([]{})));
1289     CHECK_THROWS_WITH_AS(tg1.run(tg.defer([]{})), "Attempt to schedule task_handle into different task_group", std::runtime_error);
1290 }
1291 
1292 #endif // TBB_USE_EXCEPTIONS
1293 #endif //__TBB_PREVIEW_TASK_GROUP_EXTENSIONS
1294