package boomer import ( "sync" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/pkg/boomer/grpc/messager" ) type HitOutput struct { onStart bool onEvent bool onStop bool } func (o *HitOutput) OnStart() { o.onStart = true } func (o *HitOutput) OnEvent(data map[string]interface{}) { o.onEvent = true } func (o *HitOutput) OnStop() { o.onStop = true } func TestSafeRun(t *testing.T) { runner := &runner{} runner.safeRun(func() { panic("Runner will catch this panic") }) } func TestOutputOnStart(t *testing.T) { hitOutput := &HitOutput{} hitOutput2 := &HitOutput{} runner := &runner{} runner.addOutput(hitOutput) runner.addOutput(hitOutput2) runner.outputOnStart() if !hitOutput.onStart { t.Error("hitOutput's OnStart has not been called") } if !hitOutput2.onStart { t.Error("hitOutput2's OnStart has not been called") } } func TestOutputOnEvent(t *testing.T) { hitOutput := &HitOutput{} hitOutput2 := &HitOutput{} runner := &runner{} runner.addOutput(hitOutput) runner.addOutput(hitOutput2) runner.outputOnEvent(nil) if !hitOutput.onEvent { t.Error("hitOutput's OnEvent has not been called") } if !hitOutput2.onEvent { t.Error("hitOutput2's OnEvent has not been called") } } func TestOutputOnStop(t *testing.T) { hitOutput := &HitOutput{} hitOutput2 := &HitOutput{} runner := &runner{} runner.addOutput(hitOutput) runner.addOutput(hitOutput2) runner.outputOnStop() if !hitOutput.onStop { t.Error("hitOutput's OnStop has not been called") } if !hitOutput2.onStop { t.Error("hitOutput2's OnStop has not been called") } } func TestLocalRunner(t *testing.T) { taskA := &Task{ Weight: 10, Fn: func() { time.Sleep(time.Second) }, Name: "TaskA", } tasks := []*Task{taskA} runner := newLocalRunner(2, 2) runner.setTasks(tasks) go runner.start() time.Sleep(4 * time.Second) runner.stop() } func TestLoopCount(t *testing.T) { taskA := &Task{ Weight: 10, Fn: func() { time.Sleep(time.Millisecond) }, Name: "TaskA", } tasks := []*Task{taskA} runner := newLocalRunner(2, 2) runner.loop = &Loop{loopCount: 4} runner.setTasks(tasks) runner.start() if !assert.Equal(t, atomic.LoadInt64(&runner.loop.loopCount), atomic.LoadInt64(&runner.loop.finishedCount)) { t.Fatal() } } func TestStopNotify(t *testing.T) { r := &localRunner{ runner: runner{ stopChan: make(chan bool), doneChan: make(chan bool), }, } go func() { <-r.stopChan close(r.doneChan) }() notifier := r.stopNotify() select { case <-notifier: t.Fatalf("received unexpected stop notification") default: } r.gracefulStop() select { case <-notifier: default: t.Fatalf("cannot receive stop notification") } } func TestSpawnWorkers(t *testing.T) { taskA := &Task{ Weight: 10, Fn: func() { time.Sleep(time.Second) }, Name: "TaskA", } tasks := []*Task{taskA} runner := newWorkerRunner("localhost", 5557) defer runner.close() runner.client = newClient("localhost", 5557, runner.nodeID) runner.reset() runner.setTasks(tasks) go runner.spawnWorkers(10, 10, runner.stopChan, runner.spawnComplete) time.Sleep(2 * time.Second) currentClients := runner.controller.getCurrentClientsNum() if currentClients != 10 { t.Error("Unexpected count", currentClients) } } func TestSpawnWorkersWithManyTasks(t *testing.T) { var lock sync.Mutex taskCalls := map[string]int{} createTask := func(name string, weight int) *Task { return &Task{ Name: name, Weight: weight, Fn: func() { lock.Lock() taskCalls[name]++ lock.Unlock() }, } } tasks := []*Task{ createTask("one hundred", 100), createTask("ten", 10), createTask("one", 1), } runner := newWorkerRunner("localhost", 5557) defer runner.close() runner.reset() runner.setTasks(tasks) runner.client = newClient("localhost", 5557, runner.nodeID) const numToSpawn int64 = 20 go runner.spawnWorkers(numToSpawn, float64(numToSpawn), runner.stopChan, runner.spawnComplete) time.Sleep(5 * time.Second) currentClients := runner.controller.getCurrentClientsNum() assert.Equal(t, numToSpawn, int64(currentClients)) lock.Lock() hundreds := taskCalls["one hundred"] tens := taskCalls["ten"] ones := taskCalls["one"] lock.Unlock() total := hundreds + tens + ones t.Logf("total tasks: %d, hundreds: %d, tens: %d, ones: %d\n", total, hundreds, tens, ones) assert.True(t, total > 111) assert.True(t, ones > 1) actPercentage := float64(ones) / float64(total) expectedPercentage := 1.0 / 111.0 if actPercentage > 4*expectedPercentage || actPercentage < 0.25*expectedPercentage { t.Errorf("Unexpected percentage of ones task: exp %v, act %v", expectedPercentage, actPercentage) } assert.True(t, tens > 10) actPercentage = float64(tens) / float64(total) expectedPercentage = 10.0 / 111.0 if actPercentage > 4*expectedPercentage || actPercentage < 0.25*expectedPercentage { t.Errorf("Unexpected percentage of tens task: exp %v, act %v", expectedPercentage, actPercentage) } assert.True(t, hundreds > 100) actPercentage = float64(hundreds) / float64(total) expectedPercentage = 100.0 / 111.0 if actPercentage > 1 || actPercentage < 0.25*expectedPercentage { t.Errorf("Unexpected percentage of hundreds task: exp %v, act %v", expectedPercentage, actPercentage) } } func TestSpawnAndStop(t *testing.T) { taskA := &Task{ Fn: func() { time.Sleep(time.Second) }, } taskB := &Task{ Fn: func() { time.Sleep(2 * time.Second) }, } tasks := []*Task{taskA, taskB} runner := newWorkerRunner("localhost", 5557) defer runner.close() runner.client = newClient("localhost", 5557, runner.nodeID) runner.setTasks(tasks) runner.setSpawnCount(10) runner.setSpawnRate(10) go runner.start() // wait for spawning goroutines time.Sleep(5 * time.Second) if runner.controller.getCurrentClientsNum() != 10 { t.Error("Number of goroutines mismatches, expected: 10, current count", runner.controller.getCurrentClientsNum()) } msg := <-runner.client.sendChannel() if msg.Type != "spawning_complete" { t.Error("Runner should send spawning_complete message when spawning completed, got", msg.Type) } go runner.stop() runner.onQuiting() msg = <-runner.client.sendChannel() if msg.Type != "quit" { t.Error("Runner should send quit message on quitting, got", msg.Type) } } func TestStop(t *testing.T) { taskA := &Task{ Fn: func() { time.Sleep(time.Second) }, } tasks := []*Task{taskA} runner := newWorkerRunner("localhost", 5557) runner.setTasks(tasks) runner.reset() runner.updateState(StateSpawning) go runner.stop() close(runner.doneChan) time.Sleep(1 * time.Second) if runner.getState() != StateStopped { t.Error("Expected runner state to be 5, was", getStateName(runner.getState())) } } func TestOnSpawnMessage(t *testing.T) { taskA := &Task{ Fn: func() { time.Sleep(time.Second) }, } runner := newWorkerRunner("localhost", 5557) defer runner.close() runner.client = newClient("localhost", 5557, runner.nodeID) runner.updateState(StateInit) runner.reset() runner.setTasks([]*Task{taskA}) runner.setSpawnCount(100) runner.setSpawnRate(100) runner.onSpawnMessage(newMessageToWorker("spawn", ProfileToBytes(&Profile{SpawnCount: 20, SpawnRate: 20}), nil, nil)) if runner.getSpawnCount() != 20 { t.Error("workers should be overwrote by onSpawnMessage, expected: 20, was:", runner.controller.spawnCount) } if runner.getSpawnRate() != 20 { t.Error("spawnRate should be overwrote by onSpawnMessage, expected: 20, was:", runner.controller.spawnRate) } runner.onMessage(newGenericMessage("stop", nil, runner.nodeID)) } func TestOnQuitMessage(t *testing.T) { runner := newWorkerRunner("localhost", 5557) runner.client = newClient("localhost", 5557, "test") runner.updateState(StateInit) runner.onMessage(newGenericMessage("quit", nil, runner.nodeID)) <-runner.closeChan runner.updateState(StateRunning) runner.reset() runner.closeChan = make(chan bool) runner.client.shutdownChan = make(chan bool) go runner.onMessage(newGenericMessage("quit", nil, runner.nodeID)) close(runner.doneChan) <-runner.closeChan runner.onQuiting() if runner.getState() != StateQuitting { t.Error("Runner's state should be StateQuitting") } runner.updateState(StateStopped) runner.closeChan = make(chan bool) runner.reset() runner.client.shutdownChan = make(chan bool) runner.onMessage(newGenericMessage("quit", nil, runner.nodeID)) <-runner.closeChan runner.onQuiting() if runner.getState() != StateQuitting { t.Error("Runner's state should be StateQuitting") } } func TestOnMessage(t *testing.T) { taskA := &Task{ Fn: func() { time.Sleep(time.Second) }, } taskB := &Task{ Fn: func() { time.Sleep(2 * time.Second) }, } tasks := []*Task{taskA, taskB} runner := newWorkerRunner("localhost", 5557) runner.client = newClient("localhost", 5557, runner.nodeID) runner.updateState(StateInit) runner.setTasks(tasks) // start spawning runner.onMessage(newMessageToWorker("spawn", ProfileToBytes(&Profile{SpawnCount: 10, SpawnRate: 10}), nil, nil)) go runner.start() msg := <-runner.client.sendChannel() if msg.Type != "spawning" { t.Error("Runner should send spawning message when starting spawn, got", msg.Type) } // spawn complete and running time.Sleep(5 * time.Second) if runner.controller.getCurrentClientsNum() != 10 { t.Error("Number of goroutines mismatches, expected: 10, current count:", runner.controller.getCurrentClientsNum()) } msg = <-runner.client.sendChannel() if msg.Type != "spawning_complete" { t.Error("Runner should send spawning_complete message when spawn completed, got", msg.Type) } if runner.getState() != StateRunning { t.Error("State of runner is not running after spawn, got", getStateName(runner.getState())) } // increase goroutines while running runner.onMessage(newMessageToWorker("rebalance", ProfileToBytes(&Profile{SpawnCount: 15, SpawnRate: 15}), nil, nil)) runner.controller.rebalance <- true time.Sleep(2 * time.Second) if runner.getState() != StateRunning { t.Error("State of runner is not running after spawn, got", getStateName(runner.getState())) } if runner.controller.getCurrentClientsNum() != 15 { t.Error("Number of goroutines mismatches, expected: 15, current count:", runner.controller.getCurrentClientsNum()) } // stop all the workers runner.onMessage(newGenericMessage("stop", nil, runner.nodeID)) if runner.getState() != StateStopped { t.Error("State of runner is not stopped, got", getStateName(runner.getState())) } msg = <-runner.client.sendChannel() if msg.Type != "client_stopped" { t.Error("Runner should send client_stopped message, got", msg.Type) } time.Sleep(3 * time.Second) // spawn again runner.onMessage(newMessageToWorker("spawn", ProfileToBytes(&Profile{SpawnCount: 10, SpawnRate: 10}), nil, nil)) go runner.start() msg = <-runner.client.sendChannel() if msg.Type != "spawning" { t.Error("Runner should send spawning message when starting spawn, got", msg.Type) } // spawn complete and running time.Sleep(5 * time.Second) if runner.controller.getCurrentClientsNum() != 10 { t.Error("Number of goroutines mismatches, expected: 10, current count:", runner.controller.getCurrentClientsNum()) } if runner.getState() != StateRunning { t.Error("State of runner is not running after spawn, got", getStateName(runner.getState())) } msg = <-runner.client.sendChannel() if msg.Type != "spawning_complete" { t.Error("Runner should send spawning_complete message when spawn completed, got", msg.Type) } // stop all the workers runner.onMessage(newGenericMessage("stop", nil, runner.nodeID)) if runner.getState() != StateStopped { t.Error("State of runner is not stopped, got", getStateName(runner.getState())) } msg = <-runner.client.sendChannel() if msg.Type != "client_stopped" { t.Error("Runner should send client_stopped message, got", msg.Type) } time.Sleep(3 * time.Second) // quit runner.onMessage(newGenericMessage("quit", nil, runner.nodeID)) } func TestClientListener(t *testing.T) { runner := newMasterRunner("localhost", 5557) defer runner.close() runner.updateState(StateInit) runner.setSpawnCount(10) runner.setSpawnRate(10) go runner.stateMachine() go runner.clientListener() runner.server.clients.Store("testID1", &WorkerNode{ID: "testID1", Heartbeat: 3, stream: make(chan *messager.StreamResponse, 10)}) runner.server.clients.Store("testID2", &WorkerNode{ID: "testID2", Heartbeat: 3, stream: make(chan *messager.StreamResponse, 10)}) runner.server.recvChannel() <- &genericMessage{ Type: typeClientReady, NodeID: "testID1", } worker1, ok := runner.server.getClients().Load("testID1") if !ok { t.Fatal("error") } workerInfo1, ok := worker1.(*WorkerNode) if !ok { t.Fatal("error") } time.Sleep(time.Second) if workerInfo1.getState() != StateInit { t.Error("State of worker runner is not init, got", workerInfo1.getState()) } runner.server.recvChannel() <- &genericMessage{ Type: typeClientStopped, NodeID: "testID2", } runner.updateState(StateRunning) worker2, ok := runner.server.getClients().Load("testID2") if !ok { t.Fatal("error") } workerInfo2, ok := worker2.(*WorkerNode) if !ok { t.Fatal("error") } time.Sleep(time.Second) if workerInfo2.getState() != StateStopped { t.Error("State of worker runner is not stopped, got", workerInfo2.getState()) } runner.server.recvChannel() <- &genericMessage{ Type: typeClientStopped, NodeID: "testID1", } time.Sleep(time.Second) if runner.getState() != StateStopped { t.Error("State of master runner is not stopped, got", getStateName(runner.getState())) } } func TestHeartbeatWorker(t *testing.T) { runner := newMasterRunner("localhost", 5557) defer runner.close() runner.updateState(StateInit) runner.setSpawnCount(10) runner.setSpawnRate(10) runner.server.clients.Store("testID1", &WorkerNode{ID: "testID1", Heartbeat: 1, State: StateInit, stream: make(chan *messager.StreamResponse, 10)}) runner.server.clients.Store("testID2", &WorkerNode{ID: "testID2", Heartbeat: 1, State: StateInit, stream: make(chan *messager.StreamResponse, 10)}) go runner.clientListener() go runner.heartbeatWorker() time.Sleep(4 * time.Second) worker1, ok := runner.server.getClients().Load("testID1") if !ok { t.Fatal() } workerInfo1, ok := worker1.(*WorkerNode) if !ok { t.Fatal() } if workerInfo1.getState() != StateMissing { t.Error("expected state of worker runner is missing, but got", getStateName(workerInfo1.getState())) } runner.server.recvChannel() <- &genericMessage{ Type: typeHeartbeat, NodeID: "testID2", Data: map[string][]byte{"state": builtin.Int64ToBytes(3)}, } worker2, ok := runner.server.getClients().Load("testID2") if !ok { t.Fatal() } workerInfo2, ok := worker2.(*WorkerNode) if !ok { t.Fatal() } time.Sleep(time.Second) if workerInfo2.getState() == StateMissing { t.Error("expected state of worker runner is not missing, but got missing") } }