github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/cmd/nup/client/task_cache_test.go (about)

     1  // Copyright 2022 Daniel Erat.
     2  // All rights reserved.
     3  
     4  package client
     5  
     6  import (
     7  	"errors"
     8  	"reflect"
     9  	"testing"
    10  	"time"
    11  )
    12  
    13  func TestTaskCache(t *testing.T) {
    14  	const maxTasks = 2
    15  	c := NewTaskCache(maxTasks)
    16  
    17  	type res struct {
    18  		k   string
    19  		v   interface{}
    20  		err error
    21  	}
    22  	done := make(chan res)
    23  
    24  	type taskCtl struct {
    25  		running <-chan struct{}               // closed if/when task is running
    26  		finish  chan<- map[string]interface{} // send to tell task to finish, close to report error
    27  	}
    28  
    29  	taskErr := errors.New("intentional error")
    30  
    31  	var gets int
    32  	get := func(itemKey, taskKey string) taskCtl {
    33  		gets++
    34  		running := make(chan struct{})
    35  		finish := make(chan map[string]interface{})
    36  		go func() {
    37  			v, err := c.Get(itemKey, taskKey, func() (map[string]interface{}, error) {
    38  				close(running)
    39  				m, ok := <-finish
    40  				if !ok {
    41  					return nil, taskErr
    42  				}
    43  				return m, nil
    44  			})
    45  			done <- res{itemKey, v, err}
    46  		}()
    47  		return taskCtl{running, finish}
    48  	}
    49  
    50  	// Returns true if ch appears to be closed.
    51  	closed := func(ch <-chan struct{}) bool {
    52  		select {
    53  		case <-ch:
    54  			return true
    55  		case <-time.After(20 * time.Millisecond):
    56  			return false
    57  		}
    58  	}
    59  
    60  	// Request '0' via a task named 'a'.
    61  	t0 := get("0", "a")
    62  	<-t0.running
    63  
    64  	// Request '1' via a duplicate task that shouldn't be run.
    65  	t1 := get("1", "a")
    66  	if closed(t1.running) {
    67  		t.Error("Duplicate task was started eventually")
    68  	}
    69  
    70  	// Request '2' and wait for it to start running.
    71  	t2 := get("2", "b")
    72  	<-t2.running
    73  
    74  	// Request '3' and check that its task doesn't start immediately (since we're already running
    75  	// two tasks).
    76  	t3 := get("3", "c")
    77  	if closed(t3.running) {
    78  		t.Error("Extra task was started")
    79  	}
    80  
    81  	close(t2.finish) // trigger error
    82  	<-t3.running
    83  	t3.finish <- map[string]interface{}{"3": "soup"}
    84  	t0.finish <- map[string]interface{}{"0": "foo", "1": "bar"}
    85  
    86  	// Request '4' via a new task with the same key as the one that returned an error.
    87  	t4 := get("4", "b")
    88  	<-t4.running
    89  	t4.finish <- map[string]interface{}{"4": "bananas"}
    90  
    91  	if closed(t1.running) {
    92  		t.Error("Duplicate task was started eventually")
    93  	}
    94  
    95  	got := make(map[string]interface{})
    96  	for i := 0; i < gets; i++ {
    97  		if r := <-done; r.err != nil {
    98  			got[r.k] = r.err
    99  		} else {
   100  			got[r.k] = r.v
   101  		}
   102  	}
   103  	if want := map[string]interface{}{
   104  		"0": "foo",
   105  		"1": "bar",
   106  		"2": taskErr,
   107  		"3": "soup",
   108  		"4": "bananas",
   109  	}; !reflect.DeepEqual(got, want) {
   110  		t.Errorf("Got %v; want %v", got, want)
   111  	}
   112  }