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 }