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, } }