github.com/songzhibin97/gkit@v1.2.13/cache/singleflight/singleflight_test.go (about)

     1  package singleflight
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"runtime"
    10  	"runtime/debug"
    11  	"strings"
    12  	"sync"
    13  	"sync/atomic"
    14  	"testing"
    15  	"time"
    16  )
    17  
    18  func TestDo(t *testing.T) {
    19  	g := NewSingleFlight()
    20  	v, err, _ := g.Do("key", func() (interface{}, error) {
    21  		return "bar", nil
    22  	})
    23  	if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want {
    24  		t.Errorf("Do = %v; want %v", got, want)
    25  	}
    26  	if err != nil {
    27  		t.Errorf("Do error = %v", err)
    28  	}
    29  }
    30  
    31  func TestDoErr(t *testing.T) {
    32  	g := NewSingleFlight()
    33  	someErr := errors.New("Some error")
    34  	v, err, _ := g.Do("key", func() (interface{}, error) {
    35  		return nil, someErr
    36  	})
    37  	if err != someErr {
    38  		t.Errorf("Do error = %v; want someErr %v", err, someErr)
    39  	}
    40  	if v != nil {
    41  		t.Errorf("unexpected non-nil value %#v", v)
    42  	}
    43  }
    44  
    45  func TestDoDupSuppress(t *testing.T) {
    46  	g := NewSingleFlight()
    47  	var wg1, wg2 sync.WaitGroup
    48  	c := make(chan string, 1)
    49  	var calls int32
    50  	fn := func() (interface{}, error) {
    51  		if atomic.AddInt32(&calls, 1) == 1 {
    52  			// First invocation.
    53  			wg1.Done()
    54  		}
    55  		v := <-c
    56  		c <- v // pump; make available for any future calls
    57  
    58  		time.Sleep(10 * time.Millisecond) // let more goroutines enter Do
    59  
    60  		return v, nil
    61  	}
    62  
    63  	const n = 10
    64  	wg1.Add(1)
    65  	for i := 0; i < n; i++ {
    66  		wg1.Add(1)
    67  		wg2.Add(1)
    68  		go func() {
    69  			defer wg2.Done()
    70  			wg1.Done()
    71  			v, err, _ := g.Do("key", fn)
    72  			if err != nil {
    73  				t.Errorf("Do error: %v", err)
    74  				return
    75  			}
    76  			if s, _ := v.(string); s != "bar" {
    77  				t.Errorf("Do = %T %v; want %q", v, v, "bar")
    78  			}
    79  		}()
    80  	}
    81  	wg1.Wait()
    82  
    83  	c <- "bar"
    84  	wg2.Wait()
    85  	if got := atomic.LoadInt32(&calls); got <= 0 || got >= n {
    86  		t.Errorf("number of calls = %d; want over 0 and less than %d", got, n)
    87  	}
    88  }
    89  
    90  func TestPanicDo(t *testing.T) {
    91  	g := NewSingleFlight()
    92  	fn := func() (interface{}, error) {
    93  		panic("invalid memory address or nil pointer dereference")
    94  	}
    95  
    96  	const n = 5
    97  	waited := int32(n)
    98  	panicCount := int32(0)
    99  	done := make(chan struct{})
   100  	for i := 0; i < n; i++ {
   101  		go func() {
   102  			defer func() {
   103  				if err := recover(); err != nil {
   104  					t.Logf("Got panic: %v\n%s", err, debug.Stack())
   105  					atomic.AddInt32(&panicCount, 1)
   106  				}
   107  
   108  				if atomic.AddInt32(&waited, -1) == 0 {
   109  					close(done)
   110  				}
   111  			}()
   112  
   113  			_, _, _ = g.Do("key", fn)
   114  		}()
   115  	}
   116  
   117  	select {
   118  	case <-done:
   119  		if panicCount != n {
   120  			t.Errorf("Expect %d panic, but got %d", n, panicCount)
   121  		}
   122  	case <-time.After(time.Second):
   123  		t.Fatalf("Do hangs")
   124  	}
   125  }
   126  
   127  func TestGoexitDo(t *testing.T) {
   128  	g := NewSingleFlight()
   129  	fn := func() (interface{}, error) {
   130  		runtime.Goexit()
   131  		return nil, nil
   132  	}
   133  
   134  	const n = 5
   135  	waited := int32(n)
   136  	done := make(chan struct{})
   137  	for i := 0; i < n; i++ {
   138  		go func() {
   139  			var err error
   140  			defer func() {
   141  				if err != nil {
   142  					t.Errorf("Error should be nil, but got: %v", err)
   143  				}
   144  				if atomic.AddInt32(&waited, -1) == 0 {
   145  					close(done)
   146  				}
   147  			}()
   148  			_, err, _ = g.Do("key", fn)
   149  		}()
   150  	}
   151  
   152  	select {
   153  	case <-done:
   154  	case <-time.After(time.Second):
   155  		t.Fatalf("Do hangs")
   156  	}
   157  }
   158  
   159  func TestPanicDoChan(t *testing.T) {
   160  	if runtime.GOOS == "js" {
   161  		t.Skipf("js does not support exec")
   162  	}
   163  
   164  	if os.Getenv("TEST_PANIC_DOCHAN") != "" {
   165  		defer func() {
   166  			_ = recover()
   167  		}()
   168  
   169  		g := NewSingleFlight()
   170  		ch := g.DoChan("", func() (interface{}, error) {
   171  			panic("Panicking in DoChan")
   172  		})
   173  		t.Log(<-ch)
   174  		t.Fatalf("DoChan unexpectedly returned")
   175  	}
   176  
   177  	t.Parallel()
   178  
   179  	cmd := exec.Command(os.Args[0], "-test.run="+t.Name(), "-test.v")
   180  	cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
   181  	out := new(bytes.Buffer)
   182  	cmd.Stdout = out
   183  	cmd.Stderr = out
   184  	if err := cmd.Start(); err != nil {
   185  		t.Fatal(err)
   186  	}
   187  
   188  	err := cmd.Wait()
   189  	t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out)
   190  	if err == nil {
   191  		t.Errorf("Test subprocess passed; want a crash due to panic in DoChan")
   192  	}
   193  	if bytes.Contains(out.Bytes(), []byte("DoChan unexpectedly")) {
   194  		t.Errorf("Test subprocess failed with an unexpected failure mode.")
   195  	}
   196  	if !bytes.Contains(out.Bytes(), []byte("Panicking in DoChan")) {
   197  		t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in DoChan")
   198  	}
   199  }
   200  
   201  func TestPanicDoSharedByDoChan(t *testing.T) {
   202  	if runtime.GOOS == "js" {
   203  		t.Skipf("js does not support exec")
   204  	}
   205  
   206  	if os.Getenv("TEST_PANIC_DOCHAN") != "" {
   207  		blocked := make(chan struct{})
   208  		unblock := make(chan struct{})
   209  
   210  		g := NewSingleFlight()
   211  		go func() {
   212  			defer func() {
   213  				_ = recover()
   214  			}()
   215  			_, _, _ = g.Do("", func() (interface{}, error) {
   216  				close(blocked)
   217  				<-unblock
   218  				panic("Panicking in Do")
   219  			})
   220  		}()
   221  
   222  		<-blocked
   223  		ch := g.DoChan("", func() (interface{}, error) {
   224  			panic("DoChan unexpectedly executed callback")
   225  		})
   226  		close(unblock)
   227  		<-ch
   228  		t.Fatalf("DoChan unexpectedly returned")
   229  	}
   230  
   231  	t.Parallel()
   232  
   233  	cmd := exec.Command(os.Args[0], "-test.run="+t.Name(), "-test.v")
   234  	cmd.Env = append(os.Environ(), "TEST_PANIC_DOCHAN=1")
   235  	out := new(bytes.Buffer)
   236  	cmd.Stdout = out
   237  	cmd.Stderr = out
   238  	if err := cmd.Start(); err != nil {
   239  		t.Fatal(err)
   240  	}
   241  
   242  	err := cmd.Wait()
   243  	t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out)
   244  	if err == nil {
   245  		t.Errorf("Test subprocess passed; want a crash due to panic in Do shared by DoChan")
   246  	}
   247  	if bytes.Contains(out.Bytes(), []byte("DoChan unexpectedly")) {
   248  		t.Errorf("Test subprocess failed with an unexpected failure mode.")
   249  	}
   250  	if !bytes.Contains(out.Bytes(), []byte("Panicking in Do")) {
   251  		t.Errorf("Test subprocess failed, but the crash isn't caused by panicking in Do")
   252  	}
   253  }