github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/manager/repro_test.go (about)

     1  // Copyright 2024 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package manager
     5  
     6  import (
     7  	"context"
     8  	"sync/atomic"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/google/syzkaller/pkg/report"
    13  	"github.com/stretchr/testify/assert"
    14  )
    15  
    16  func TestReproManager(t *testing.T) {
    17  	mock := &reproMgrMock{
    18  		run: make(chan runCallback),
    19  	}
    20  	obj := NewReproLoop(mock, 3, false)
    21  	// No reproductions until we've started.
    22  	assert.False(t, obj.CanReproMore())
    23  
    24  	ctx, done := context.WithCancel(context.Background())
    25  	complete := make(chan struct{})
    26  	go func() {
    27  		obj.Loop(ctx)
    28  		close(complete)
    29  	}()
    30  
    31  	defer func() {
    32  		done()
    33  		<-complete
    34  	}()
    35  
    36  	obj.Enqueue(&Crash{Report: &report.Report{Title: "A"}})
    37  	called := <-mock.run
    38  	assert.Equal(t, "A", called.crash.Title)
    39  
    40  	// One reproducer is running -- we can take one more.
    41  	assert.True(t, obj.CanReproMore())
    42  	assert.EqualValues(t, 2, mock.reserved.Load())
    43  	obj.Enqueue(&Crash{Report: &report.Report{Title: "B"}})
    44  	called2 := <-mock.run
    45  	assert.Equal(t, "B", called2.crash.Title)
    46  
    47  	assert.False(t, obj.CanReproMore())
    48  	assert.Len(t, obj.Reproducing(), 2)
    49  	assert.EqualValues(t, 3, mock.reserved.Load())
    50  
    51  	// Pretend that reproducers have finished.
    52  	called.ret <- &ReproResult{Crash: &Crash{FromHub: true}}
    53  	called2.ret <- &ReproResult{Crash: &Crash{FromHub: true}}
    54  
    55  	mock.onVMShutdown(t, obj)
    56  }
    57  
    58  func TestReproOrder(t *testing.T) {
    59  	mock := &reproMgrMock{
    60  		run: make(chan runCallback),
    61  	}
    62  	obj := NewReproLoop(mock, 1, false)
    63  
    64  	// The right order is A B C.
    65  	crashes := []*Crash{
    66  		{
    67  			Report:        &report.Report{Title: "A"},
    68  			FromDashboard: true,
    69  			Manual:        true,
    70  		},
    71  		{
    72  			Report:        &report.Report{Title: "B"},
    73  			FromDashboard: true,
    74  		},
    75  		{
    76  			Report:  &report.Report{Title: "C"},
    77  			FromHub: true,
    78  		},
    79  	}
    80  
    81  	obj.Enqueue(crashes[2])
    82  	obj.Enqueue(crashes[1])
    83  	obj.Enqueue(crashes[0])
    84  	obj.Enqueue(crashes[1])
    85  	obj.Enqueue(crashes[0])
    86  	obj.Enqueue(crashes[2])
    87  
    88  	ctx, cancel := context.WithCancel(context.Background())
    89  	defer cancel()
    90  	go obj.Loop(ctx)
    91  
    92  	for i := 0; i < len(crashes)*2; i++ {
    93  		called := <-mock.run
    94  		assert.Equal(t, crashes[i%len(crashes)], called.crash)
    95  		called.ret <- &ReproResult{}
    96  	}
    97  }
    98  
    99  func TestReproRWRace(t *testing.T) {
   100  	var reproProgExist atomic.Bool
   101  	mock := &reproMgrMock{
   102  		run: make(chan runCallback),
   103  		needReproCb: func(_ *Crash) bool {
   104  			return !reproProgExist.Load()
   105  		},
   106  	}
   107  	obj := NewReproLoop(mock, 3, false)
   108  
   109  	ctx, cancel := context.WithCancel(context.Background())
   110  	defer cancel()
   111  
   112  	go obj.Loop(ctx) // calls runRepro()
   113  
   114  	obj.Enqueue(&Crash{Report: &report.Report{Title: "A"}})
   115  	obj.Enqueue(&Crash{Report: &report.Report{Title: "A"}})
   116  
   117  	assert.True(t, mock.NeedRepro(nil))
   118  	called := <-mock.run
   119  	// Pretend that processRepro() is finished and
   120  	// we've written "repro.prog" to the disk.
   121  	reproProgExist.Store(true)
   122  	assert.False(t, mock.NeedRepro(nil))
   123  	called.ret <- &ReproResult{}
   124  	assert.True(t, obj.CanReproMore())
   125  
   126  	// The second repro process will never be started.
   127  	mock.onVMShutdown(t, obj)
   128  }
   129  
   130  func TestCancelRunningRepro(t *testing.T) {
   131  	mock := &reproMgrMock{
   132  		run: make(chan runCallback),
   133  	}
   134  	obj := NewReproLoop(mock, 1, false)
   135  	ctx, done := context.WithCancel(context.Background())
   136  	complete := make(chan struct{})
   137  	go func() {
   138  		obj.Loop(ctx)
   139  		close(complete)
   140  	}()
   141  
   142  	defer func() {
   143  		<-complete
   144  	}()
   145  
   146  	obj.Enqueue(&Crash{Report: &report.Report{Title: "A"}})
   147  	obj.Enqueue(&Crash{Report: &report.Report{Title: "B"}})
   148  	<-mock.run
   149  	done()
   150  }
   151  
   152  func TestEnqueueTriggersRepro(t *testing.T) {
   153  	mock := &reproMgrMock{
   154  		run: make(chan runCallback),
   155  		needReproCb: func(crash *Crash) bool {
   156  			return crash.FullTitle() == "C"
   157  		},
   158  	}
   159  	obj := NewReproLoop(mock, 1, false)
   160  	obj.Enqueue(&Crash{Report: &report.Report{Title: "A"}, Manual: true})
   161  	obj.Enqueue(&Crash{Report: &report.Report{Title: "B"}, Manual: true})
   162  	obj.Enqueue(&Crash{Report: &report.Report{Title: "C"}})
   163  
   164  	ctx, done := context.WithCancel(context.Background())
   165  	complete := make(chan struct{})
   166  	go func() {
   167  		obj.Loop(ctx)
   168  		close(complete)
   169  	}()
   170  
   171  	defer func() {
   172  		<-complete
   173  	}()
   174  	// The test will hang if the loop never picks up the title C.
   175  	crash := <-mock.run
   176  	assert.Equal(t, "C", crash.crash.FullTitle())
   177  	done()
   178  }
   179  
   180  type reproMgrMock struct {
   181  	reserved    atomic.Int64
   182  	run         chan runCallback
   183  	needReproCb func(*Crash) bool
   184  }
   185  
   186  type runCallback struct {
   187  	crash *Crash
   188  	ret   chan *ReproResult
   189  }
   190  
   191  // Wait until the number of reserved VMs goes to 0.
   192  func (m *reproMgrMock) onVMShutdown(t *testing.T, reproLoop *ReproLoop) {
   193  	for i := 0; i < 100; i++ {
   194  		if m.reserved.Load() == 0 {
   195  			assert.True(t, reproLoop.CanReproMore())
   196  			assert.True(t, reproLoop.Empty())
   197  			return
   198  		}
   199  		time.Sleep(10 * time.Millisecond)
   200  	}
   201  	t.Fatal("reserved VMs must have dropped to 0")
   202  }
   203  
   204  func (m *reproMgrMock) RunRepro(ctx context.Context, crash *Crash) *ReproResult {
   205  	retCh := make(chan *ReproResult)
   206  	select {
   207  	case m.run <- runCallback{crash: crash, ret: retCh}:
   208  	case <-ctx.Done():
   209  		return &ReproResult{}
   210  	}
   211  	var ret *ReproResult
   212  	select {
   213  	case ret = <-retCh:
   214  	case <-ctx.Done():
   215  		return &ReproResult{}
   216  	}
   217  	close(retCh)
   218  	return ret
   219  }
   220  
   221  func (m *reproMgrMock) NeedRepro(crash *Crash) bool {
   222  	if m.needReproCb != nil {
   223  		return m.needReproCb(crash)
   224  	}
   225  	return true
   226  }
   227  
   228  func (m *reproMgrMock) ResizeReproPool(VMs int) {
   229  	m.reserved.Store(int64(VMs))
   230  }