Files
monibuca/plugin/test/index.go
2025-09-26 15:57:26 +08:00

270 lines
6.6 KiB
Go

package plugin_test
import (
_ "embed"
"errors"
"fmt"
"net/http"
"reflect"
"slices"
"strings"
"time"
task "github.com/langhuihui/gotask"
"m7s.live/v5"
"m7s.live/v5/pkg/util"
"m7s.live/v5/plugin/test/pb"
)
const (
TestCaseStatusInit TestCaseStatus = "init"
TestCaseStatusStarting TestCaseStatus = "starting"
TestCaseStatusRunning TestCaseStatus = "running"
TestCaseStatusSuccess TestCaseStatus = "success"
TestCaseStatusFailed TestCaseStatus = "failed"
)
func (f *TestTaskFactory) Register(action string, taskCreator func(*TestCase, TestTaskConfig) task.ITask) {
f.tasks[action] = taskCreator
}
func (f *TestTaskFactory) Create(taskConfig TestTaskConfig, scenario *TestCase) (task.ITask, error) {
if taskCreator, exists := f.tasks[taskConfig.Action]; exists {
return taskCreator(scenario, taskConfig), nil
}
return nil, fmt.Errorf("no task registered for action: %s", taskConfig)
}
var testTaskFactory = TestTaskFactory{
tasks: make(map[string]func(*TestCase, TestTaskConfig) task.ITask),
}
type (
TestTaskFactory struct {
tasks map[string]func(*TestCase, TestTaskConfig) task.ITask
}
TestTaskConfig struct {
Action string `json:"action"`
Delay time.Duration `json:"delay"`
Format string `json:"format"`
ServerAddr string `json:"serverAddr" default:"localhost"`
Input string `json:"input"`
StreamPath string `json:"streamPath"`
}
TestCaseStatus string
TestConfig struct {
Name string `json:"name"`
Description string `json:"description"`
VideoCodec string `json:"videoCodec" default:"h264"`
AudioCodec string `json:"audioCodec" default:"aac"`
VideoOnly bool `json:"videoOnly"`
AudioOnly bool `json:"audioOnly"`
Tags []string `json:"tags"`
Timeout time.Duration `json:"timeout" default:"30s"`
Tasks []TestTaskConfig `json:"tasks"`
}
TestCase struct {
*task.Job `json:"-"`
*TestConfig
Plugin *TestPlugin `json:"-"`
Status TestCaseStatus `json:"status"`
StartTime int64 `json:"startTime"`
EndTime int64 `json:"endTime"`
Duration int32 `json:"duration"`
ErrorMsg string `json:"errorMsg"`
Logs string `json:"logs"`
}
TestPlugin struct {
pb.UnimplementedApiServer
m7s.Plugin
Cases map[string]TestConfig
testCases map[string]*TestCase
flushSSE chan struct{}
// Stress 测试相关字段
pushers util.Collection[string, *m7s.PushJob]
pullers util.Collection[string, *m7s.PullJob]
}
TestBaseTask struct {
task.Task
testCase *TestCase
TestTaskConfig
}
)
func (ts *TestCase) Start() (err error) {
ts.Status = TestCaseStatusStarting
ts.StartTime = time.Now().Unix()
return nil
}
func (ts *TestCase) Go() (err error) {
ts.Status = TestCaseStatusRunning
ts.Plugin.FlushSSE()
subTaskSelect := []reflect.SelectCase{
{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(time.After(ts.Timeout)),
},
{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(ts.Done()),
},
}
var subTask []task.ITask
for _, taskConfig := range ts.Tasks {
if taskConfig.StreamPath == "" {
taskConfig.StreamPath = fmt.Sprintf("test/%d", ts.ID)
}
if taskConfig.Input != "" && !strings.Contains(taskConfig.Input, ".") {
taskConfig.Input = fmt.Sprintf("%s/%d", taskConfig.Input, ts.ID)
}
t, err := testTaskFactory.Create(taskConfig, ts)
if err != nil {
ts.Status = TestCaseStatusFailed
ts.ErrorMsg = fmt.Sprintf("Failed to create test task: %v", err)
ts.Plugin.FlushSSE()
return err
}
if taskConfig.Delay > 0 {
subTask = append(subTask, t)
subTaskSelect = append(subTaskSelect, reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(time.After(taskConfig.Delay)),
})
} else {
ts.AddDependTask(t)
}
}
for {
chosen, _, recvOK := reflect.Select(subTaskSelect)
switch chosen {
case 0:
ts.Stop(task.ErrTimeout)
case 1:
if errors.Is(ts.StopReason(), task.ErrTaskComplete) {
ts.Status = TestCaseStatusSuccess
} else {
ts.Status = TestCaseStatusFailed
ts.ErrorMsg = ts.StopReason().Error()
}
ts.Plugin.FlushSSE()
return nil
default:
if recvOK {
ts.AddDependTask(subTask[chosen-2])
}
}
}
}
// Dispose 任务停止
func (ts *TestCase) Dispose() {
if ts.ErrorMsg == "" {
ts.Status = TestCaseStatusSuccess
} else {
ts.Status = TestCaseStatusFailed
}
ts.EndTime = time.Now().Unix()
ts.Duration = int32(time.Now().Unix() - ts.StartTime)
ts.Plugin.FlushSSE()
}
func (ts *TestCase) Write(buf []byte) (int, error) {
ts.Logs += time.Now().Format("2006-01-02 15:04:05") + " " + string(buf) + "\n"
return len(buf), nil
}
// GetTestCaseFromCache 从缓存获取测试用例
func (p *TestPlugin) GetTestCaseFromCache(name string) (tc *TestCase, exists bool) {
p.Call(func() {
tc, exists = p.testCases[name]
})
return
}
func (p *TestPlugin) FlushSSE() {
select {
case p.flushSSE <- struct{}{}:
default:
}
}
type TestCaseFilter struct {
Tags []string
Status TestCaseStatus
Category string
TestType string
}
var StatusOrder = [...]TestCaseStatus{
TestCaseStatusRunning,
TestCaseStatusStarting,
TestCaseStatusFailed,
TestCaseStatusSuccess,
TestCaseStatusInit,
}
func (p *TestPlugin) GetTestCasesFromCache(filter TestCaseFilter) (cases []*TestCase) {
p.Call(func() {
for _, tc := range p.testCases {
// 标签过滤
if len(filter.Tags) > 0 {
if !slices.ContainsFunc(filter.Tags, func(tag string) bool {
return slices.Contains(tc.Tags, tag)
}) {
continue
}
}
if filter.Status != "" && tc.Status != filter.Status {
continue
}
cases = append(cases, tc)
}
})
slices.SortFunc(cases, func(a, b *TestCase) int {
if a.Status == b.Status {
return strings.Compare(a.Name, b.Name)
}
for _, status := range StatusOrder {
if a.Status == status {
return -1
}
if b.Status == status {
return 1
}
}
return 0
})
return
}
//go:embed default.yaml
var defaultYaml m7s.DefaultYaml
var _ = m7s.InstallPlugin[TestPlugin](m7s.PluginMeta{
ServiceDesc: &pb.Api_ServiceDesc,
RegisterGRPCHandler: pb.RegisterApiHandler,
DefaultYaml: defaultYaml,
})
func (p *TestPlugin) Start() error {
p.testCases = make(map[string]*TestCase)
for name, tc := range p.Cases {
tc.Name = name
p.testCases[name] = &TestCase{
TestConfig: &tc,
Plugin: p,
Status: TestCaseStatusInit,
}
}
p.flushSSE = make(chan struct{}, 1)
return nil
}
func (p *TestPlugin) RegisterHandler() map[string]http.HandlerFunc {
return map[string]http.HandlerFunc{
"/sse/cases": p.GetTestCaseSSE,
}
}