github.com/safing/portbase@v0.19.5/modules/microtasks_test.go (about)

     1  package modules
     2  
     3  import (
     4  	"context"
     5  	"runtime"
     6  	"strings"
     7  	"sync"
     8  	"sync/atomic"
     9  	"testing"
    10  	"time"
    11  )
    12  
    13  var (
    14  	mtTestName = "microtask test"
    15  	mtModule   = initNewModule("microtask test module", nil, nil, nil)
    16  )
    17  
    18  func init() {
    19  	go microTaskScheduler()
    20  }
    21  
    22  func TestMicroTaskWaiting(t *testing.T) { //nolint:paralleltest // Too much interference expected.
    23  
    24  	// Check if the state is clean.
    25  	if atomic.LoadInt32(microTasks) != 0 {
    26  		t.Fatalf("cannot start test with dirty state: %d microtasks", atomic.LoadInt32(microTasks))
    27  	}
    28  
    29  	// skip
    30  	if testing.Short() {
    31  		t.Skip("skipping test in short mode, as it is not fully deterministic")
    32  	}
    33  
    34  	// init
    35  	mtwWaitGroup := new(sync.WaitGroup)
    36  	mtwOutputChannel := make(chan string, 100)
    37  	mtwExpectedOutput := "1234567"
    38  	mtwSleepDuration := 10 * time.Millisecond
    39  
    40  	// TEST
    41  	mtwWaitGroup.Add(4)
    42  
    43  	// ensure we only execute one microtask at once
    44  	atomic.StoreInt32(microTasksThreshhold, 1)
    45  
    46  	// High Priority - slot 1-5
    47  	go func() {
    48  		defer mtwWaitGroup.Done()
    49  		// exec at slot 1
    50  		_ = mtModule.RunHighPriorityMicroTask(mtTestName, func(ctx context.Context) error {
    51  			mtwOutputChannel <- "1" // slot 1
    52  			time.Sleep(mtwSleepDuration * 5)
    53  			mtwOutputChannel <- "2" // slot 5
    54  			return nil
    55  		})
    56  	}()
    57  
    58  	time.Sleep(mtwSleepDuration * 1)
    59  
    60  	// clear clearances
    61  	_ = mtModule.RunHighPriorityMicroTask(mtTestName, func(ctx context.Context) error {
    62  		return nil
    63  	})
    64  
    65  	// Low Priority - slot 16
    66  	go func() {
    67  		defer mtwWaitGroup.Done()
    68  		// exec at slot 2
    69  		_ = mtModule.RunLowPriorityMicroTask(mtTestName, 0, func(ctx context.Context) error {
    70  			mtwOutputChannel <- "7" // slot 16
    71  			return nil
    72  		})
    73  	}()
    74  
    75  	time.Sleep(mtwSleepDuration * 1)
    76  
    77  	// High Priority - slot 10-15
    78  	go func() {
    79  		defer mtwWaitGroup.Done()
    80  		time.Sleep(mtwSleepDuration * 8)
    81  		// exec at slot 10
    82  		_ = mtModule.RunHighPriorityMicroTask(mtTestName, func(ctx context.Context) error {
    83  			mtwOutputChannel <- "4" // slot 10
    84  			time.Sleep(mtwSleepDuration * 5)
    85  			mtwOutputChannel <- "6" // slot 15
    86  			return nil
    87  		})
    88  	}()
    89  
    90  	// Medium Priority - slot 6-13
    91  	go func() {
    92  		defer mtwWaitGroup.Done()
    93  		// exec at slot 3
    94  		_ = mtModule.RunMicroTask(mtTestName, 0, func(ctx context.Context) error {
    95  			mtwOutputChannel <- "3" // slot 6
    96  			time.Sleep(mtwSleepDuration * 7)
    97  			mtwOutputChannel <- "5" // slot 13
    98  			return nil
    99  		})
   100  	}()
   101  
   102  	// wait for test to finish
   103  	mtwWaitGroup.Wait()
   104  
   105  	// collect output
   106  	close(mtwOutputChannel)
   107  	completeOutput := ""
   108  	for s := <-mtwOutputChannel; s != ""; s = <-mtwOutputChannel {
   109  		completeOutput += s
   110  	}
   111  	// check if test succeeded
   112  	t.Logf("microTask wait order: %s", completeOutput)
   113  	if completeOutput != mtwExpectedOutput {
   114  		t.Errorf("MicroTask waiting test failed, expected sequence %s, got %s", mtwExpectedOutput, completeOutput)
   115  	}
   116  
   117  	// Check if the state is clean.
   118  	time.Sleep(10 * time.Millisecond)
   119  	if atomic.LoadInt32(microTasks) != 0 {
   120  		t.Fatalf("test ends with dirty state: %d microtasks", atomic.LoadInt32(microTasks))
   121  	}
   122  }
   123  
   124  // Test Microtask ordering.
   125  
   126  // Microtask test globals.
   127  
   128  var (
   129  	mtoWaitGroup     sync.WaitGroup
   130  	mtoOutputChannel chan string
   131  	mtoWaitCh        chan struct{}
   132  )
   133  
   134  // Microtask test functions.
   135  
   136  func highPrioTaskTester() {
   137  	defer mtoWaitGroup.Done()
   138  	<-mtoWaitCh
   139  	_ = mtModule.RunHighPriorityMicroTask(mtTestName, func(ctx context.Context) error {
   140  		mtoOutputChannel <- "0"
   141  		time.Sleep(2 * time.Millisecond)
   142  		return nil
   143  	})
   144  }
   145  
   146  func highPrioSignalledTaskTester() {
   147  	defer mtoWaitGroup.Done()
   148  	<-mtoWaitCh
   149  	go func() {
   150  		done := mtModule.SignalHighPriorityMicroTask()
   151  		defer done()
   152  
   153  		mtoOutputChannel <- "0"
   154  		time.Sleep(2 * time.Millisecond)
   155  	}()
   156  }
   157  
   158  func mediumPrioTaskTester() {
   159  	defer mtoWaitGroup.Done()
   160  	<-mtoWaitCh
   161  	_ = mtModule.RunMicroTask(mtTestName, 0, func(ctx context.Context) error {
   162  		mtoOutputChannel <- "1"
   163  		time.Sleep(2 * time.Millisecond)
   164  		return nil
   165  	})
   166  }
   167  
   168  func mediumPrioSignalledTaskTester() {
   169  	defer mtoWaitGroup.Done()
   170  	<-mtoWaitCh
   171  	go func() {
   172  		done := mtModule.SignalMicroTask(0)
   173  		defer done()
   174  
   175  		mtoOutputChannel <- "1"
   176  		time.Sleep(2 * time.Millisecond)
   177  	}()
   178  }
   179  
   180  func lowPrioTaskTester() {
   181  	defer mtoWaitGroup.Done()
   182  	<-mtoWaitCh
   183  	_ = mtModule.RunLowPriorityMicroTask(mtTestName, 0, func(ctx context.Context) error {
   184  		mtoOutputChannel <- "2"
   185  		time.Sleep(2 * time.Millisecond)
   186  		return nil
   187  	})
   188  }
   189  
   190  func lowPrioSignalledTaskTester() {
   191  	defer mtoWaitGroup.Done()
   192  	<-mtoWaitCh
   193  	go func() {
   194  		done := mtModule.SignalLowPriorityMicroTask(0)
   195  		defer done()
   196  
   197  		mtoOutputChannel <- "2"
   198  		time.Sleep(2 * time.Millisecond)
   199  	}()
   200  }
   201  
   202  func TestMicroTaskOrdering(t *testing.T) { //nolint:paralleltest // Too much interference expected.
   203  
   204  	// Check if the state is clean.
   205  	if atomic.LoadInt32(microTasks) != 0 {
   206  		t.Fatalf("cannot start test with dirty state: %d microtasks", atomic.LoadInt32(microTasks))
   207  	}
   208  
   209  	// skip
   210  	if testing.Short() {
   211  		t.Skip("skipping test in short mode, as it is not fully deterministic")
   212  	}
   213  
   214  	// Only allow a single concurrent task for testing.
   215  	atomic.StoreInt32(microTasksThreshhold, 1)
   216  	defer SetMaxConcurrentMicroTasks(runtime.GOMAXPROCS(0))
   217  
   218  	// init
   219  	mtoOutputChannel = make(chan string, 100)
   220  	mtoWaitCh = make(chan struct{})
   221  
   222  	// TEST
   223  
   224  	// init all in waiting state
   225  	for i := 0; i < 5; i++ {
   226  		mtoWaitGroup.Add(6)
   227  		go lowPrioTaskTester()
   228  		go lowPrioSignalledTaskTester()
   229  		go mediumPrioTaskTester()
   230  		go mediumPrioSignalledTaskTester()
   231  		go highPrioTaskTester()
   232  		go highPrioSignalledTaskTester()
   233  	}
   234  
   235  	// wait for all goroutines to be ready
   236  	time.Sleep(10 * time.Millisecond)
   237  
   238  	// sync all goroutines
   239  	close(mtoWaitCh)
   240  	// trigger
   241  	select {
   242  	case microTaskFinished <- struct{}{}:
   243  	default:
   244  	}
   245  
   246  	// wait for test to finish
   247  	mtoWaitGroup.Wait()
   248  
   249  	// collect output
   250  	close(mtoOutputChannel)
   251  	completeOutput := ""
   252  	for s := range mtoOutputChannel {
   253  		completeOutput += s
   254  	}
   255  
   256  	// check if test succeeded
   257  	t.Logf("microTask exec order: %s", completeOutput)
   258  	if !strings.Contains(completeOutput, "000") ||
   259  		!strings.Contains(completeOutput, "1111") ||
   260  		!strings.Contains(completeOutput, "22222") {
   261  		t.Errorf("MicroTask ordering test failed, output was %s. This happens occasionally, please run the test multiple times to verify", completeOutput)
   262  	}
   263  
   264  	// Check if the state is clean.
   265  	time.Sleep(10 * time.Millisecond)
   266  	if atomic.LoadInt32(microTasks) != 0 {
   267  		t.Fatalf("test ends with dirty state: %d microtasks", atomic.LoadInt32(microTasks))
   268  	}
   269  }