github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/controller/processor_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 main 5 6 import ( 7 "context" 8 "fmt" 9 "sync" 10 "testing" 11 "time" 12 13 "github.com/google/syzkaller/syz-cluster/pkg/api" 14 "github.com/google/syzkaller/syz-cluster/pkg/app" 15 "github.com/google/syzkaller/syz-cluster/pkg/controller" 16 "github.com/google/syzkaller/syz-cluster/pkg/db" 17 "github.com/google/syzkaller/syz-cluster/pkg/workflow" 18 "github.com/stretchr/testify/assert" 19 ) 20 21 // It's a bit too long for a unit test, but it captures the whole main scenario of operation. 22 func TestProcessor(t *testing.T) { 23 workflows := newMockedWorkflows() 24 processor, client, ctx := prepareProcessorTest(t, workflows) 25 26 // Start the loop. 27 var wg sync.WaitGroup 28 ctx2, cancel := context.WithCancel(ctx) 29 wg.Add(1) 30 go func() { 31 processor.Loop(ctx2) 32 wg.Done() 33 }() 34 35 // Add some series. 36 var allSeries []*api.Series 37 for i := 0; i < 10; i++ { 38 id := fmt.Sprintf("series-%d", i) 39 allSeries = append(allSeries, &api.Series{ 40 ExtID: id, 41 Title: id, 42 }) 43 } 44 for _, series := range allSeries[0:5] { 45 controller.UploadTestSeries(t, ctx, client, series) 46 } 47 48 // Let some workflows finish. 49 for i := 0; i < 2; i++ { 50 workflows.finish <- struct{}{} 51 } 52 53 awaitFinishedSessions(t, processor.seriesRepo, 2) 54 55 // Emulate the service restart by aborting the loop. 56 // This may break the execution in arbitrary places, which actually resembles the environment in which the code 57 // will actually work. The bugs it triggers may be difficult to reproduce though. 58 cancel() 59 wg.Wait() 60 61 ctx3, cancel := context.WithCancel(ctx) 62 wg.Add(1) 63 defer wg.Wait() 64 go func() { 65 processor.Loop(ctx3) 66 wg.Done() 67 }() 68 69 // Add some more series. 70 for _, series := range allSeries[5:10] { 71 controller.UploadTestSeries(t, ctx, client, series) 72 } 73 74 // Finish all of them. 75 for i := 0; i < 8; i++ { 76 workflows.finish <- struct{}{} 77 } 78 79 awaitFinishedSessions(t, processor.seriesRepo, 10) 80 cancel() 81 } 82 83 func TestFinishRunningSteps(t *testing.T) { 84 workflows := newMockedWorkflows() 85 processor, client, ctx := prepareProcessorTest(t, workflows) 86 87 // Start the loop. 88 var wg sync.WaitGroup 89 ctx2, cancel := context.WithCancel(ctx) 90 wg.Add(1) 91 go func() { 92 processor.Loop(ctx2) 93 wg.Done() 94 }() 95 96 series := &api.Series{ 97 ExtID: "ext-id", 98 Title: "title", 99 } 100 ids := controller.UploadTestSeries(t, ctx, client, series) 101 buildResp := controller.UploadTestBuild(t, ctx, client, &api.Build{ 102 Arch: "amd64", 103 TreeName: "mainline", 104 ConfigName: "config", 105 CommitHash: "abcd", 106 }) 107 err := client.UploadTestResult(ctx, &api.TestResult{ 108 SessionID: ids.SessionID, 109 BaseBuildID: buildResp.ID, 110 TestName: "test", 111 Result: api.TestRunning, 112 }) 113 assert.NoError(t, err) 114 115 // Let the workflow finish. 116 workflows.finish <- struct{}{} 117 awaitFinishedSessions(t, processor.seriesRepo, 1) 118 cancel() 119 120 // Verify that the session test is finished. 121 // A bit hacky, but it works. 122 list, err := processor.sessionTestRepo.BySessionRaw(ctx, ids.SessionID) 123 assert.NoError(t, err) 124 assert.Equal(t, api.TestError, list[0].Result) 125 } 126 127 func awaitFinishedSessions(t *testing.T, seriesRepo *db.SeriesRepository, wantFinished int) { 128 t.Logf("awaiting %d finished sessions", wantFinished) 129 deadline := time.Second * 2 130 interval := time.Second / 10 131 for i := 0; i < int(deadline/interval); i++ { 132 time.Sleep(interval) 133 134 list, err := seriesRepo.ListLatest(context.Background(), db.SeriesFilter{}, time.Time{}) 135 assert.NoError(t, err) 136 withFinishedSeries := 0 137 for _, item := range list { 138 if item.Session == nil { 139 continue 140 } 141 if item.Session.FinishedAt.IsNull() { 142 continue 143 } 144 withFinishedSeries++ 145 } 146 t.Logf("have %d finished", withFinishedSeries) 147 if withFinishedSeries == wantFinished { 148 return 149 } 150 } 151 t.Fatalf("never reached %d finished series", wantFinished) 152 } 153 154 type mockedWorkflows struct { 155 workflow.MockService 156 finish chan struct{} 157 created map[string]struct{} 158 } 159 160 func newMockedWorkflows() *mockedWorkflows { 161 obj := mockedWorkflows{ 162 finish: make(chan struct{}), 163 created: make(map[string]struct{}), 164 } 165 obj.PollDelayValue = time.Millisecond 166 obj.OnStart = func(id string) error { 167 obj.created[id] = struct{}{} 168 return nil 169 } 170 obj.OnStatus = func(id string) (workflow.Status, []byte, error) { 171 _, ok := obj.created[id] 172 if !ok { 173 return workflow.StatusNotFound, nil, nil 174 } 175 finished := false 176 select { 177 case <-obj.finish: 178 finished = true 179 default: 180 } 181 if finished { 182 return workflow.StatusFinished, nil, nil 183 } 184 return workflow.StatusRunning, nil, nil 185 } 186 return &obj 187 } 188 189 func prepareProcessorTest(t *testing.T, workflows workflow.Service) (*SeriesProcessor, 190 *api.Client, context.Context) { 191 env, ctx := app.TestEnvironment(t) 192 client := controller.TestServer(t, env) 193 return &SeriesProcessor{ 194 seriesRepo: db.NewSeriesRepository(env.Spanner), 195 sessionRepo: db.NewSessionRepository(env.Spanner), 196 sessionTestRepo: db.NewSessionTestRepository(env.Spanner), 197 workflows: workflows, 198 dbPollInterval: time.Second / 10, 199 parallelWorkflows: 2, 200 }, client, ctx 201 }