package taskworker import ( "context" "testing" runnerv1 "gitea.dev/actionslib/runner/v1" "git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskassignment" "git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskidentity" ) type fakeBackend struct { executor *Executor created int bound int deleted int } type fakeBootstrap struct{} func (fakeBootstrap) Environment(taskassignment.Assignment) (map[string]string, error) { return map[string]string{"CI_RUNNER_CAPABILITY": "capability"}, nil } func (b *fakeBackend) Find(context.Context, string) (*Executor, error) { return b.executor, nil } func (b *fakeBackend) Create(_ context.Context, _ taskassignment.Assignment, _ LaunchSpec) (*Executor, error) { b.created++ b.executor = &Executor{Name: "executor", IdentityTarget: "pod-uid", Phase: PhaseRunning} return b.executor, nil } func (b *fakeBackend) BindIdentity(context.Context, *Executor, taskidentity.Identity) error { b.bound++ return nil } func (b *fakeBackend) Delete(context.Context, *Executor) error { b.deleted++ b.executor = nil return nil } type fakeTasks struct { terminal bool reported []Phase } func (t *fakeTasks) Terminal(context.Context, int64) (bool, error) { return t.terminal, nil } func (t *fakeTasks) Report(_ context.Context, _ int64, phase Phase) error { t.reported = append(t.reported, phase) t.terminal = true return nil } func assignment() taskassignment.Assignment { return taskassignment.Assignment{ ID: "gitea-task-42", Backend: taskassignment.BackendPod, Task: &runnerv1.Task{Id: 42}, Identity: taskidentity.Identity{ Repository: "owner/repo", Task: "publish", SPIFFEID: "spiffe://ddupan.top/ci/owner/repo/publish", }, } } func TestHandleRecoversExistingExecutorWithoutCreatingAnother(t *testing.T) { backend := &fakeBackend{executor: &Executor{Name: "existing", IdentityTarget: "uid", Phase: PhaseRunning}} worker := Worker{Backend: backend, Tasks: &fakeTasks{}, Bootstrap: fakeBootstrap{}} done, err := worker.Handle(context.Background(), assignment()) if err != nil || done { t.Fatalf("Handle() = (%v, %v), want pending", done, err) } if backend.created != 0 || backend.bound != 1 { t.Fatalf("created=%d bound=%d", backend.created, backend.bound) } } func TestAcceptAcknowledgesAfterBackendAndIdentityAreDurable(t *testing.T) { backend := &fakeBackend{} var events []Event worker := Worker{Backend: backend, Bootstrap: fakeBootstrap{}, OnEvent: func(event Event) { events = append(events, event) }} accepted, err := worker.Accept(context.Background(), assignment()) if err != nil || !accepted { t.Fatalf("Accept() = (%v, %v), want accepted", accepted, err) } if backend.created != 1 || backend.bound != 1 || backend.deleted != 0 { t.Fatalf("created=%d bound=%d deleted=%d", backend.created, backend.bound, backend.deleted) } want := []string{"executor_absent", "executor_created", "identity_bound"} if len(events) != len(want) { t.Fatalf("events = %#v", events) } for index := range want { if events[index].Name != want[index] || events[index].AssignmentID != "gitea-task-42" { t.Fatalf("event[%d] = %#v", index, events[index]) } } if events[1].Executor != "executor" || events[1].Phase != PhaseRunning { t.Fatalf("created event = %#v", events[1]) } } func TestAcceptRetriesWhileBackendIdentityTargetIsUnavailable(t *testing.T) { backend := &fakeBackend{executor: &Executor{Name: "pending", Phase: PhasePending}} worker := Worker{Backend: backend, Bootstrap: fakeBootstrap{}} accepted, err := worker.Accept(context.Background(), assignment()) if err != nil || accepted { t.Fatalf("Accept() = (%v, %v), want retry", accepted, err) } if backend.bound != 0 { t.Fatalf("identity bindings = %d", backend.bound) } } func TestHandleReportsBeforeCleanupAndBecomesRecoverable(t *testing.T) { backend := &fakeBackend{executor: &Executor{Name: "finished", IdentityTarget: "uid", Phase: PhaseSucceeded}} tasks := &fakeTasks{} worker := Worker{Backend: backend, Tasks: tasks, Bootstrap: fakeBootstrap{}} done, err := worker.Handle(context.Background(), assignment()) if err != nil || !done { t.Fatalf("Handle() = (%v, %v), want done", done, err) } if len(tasks.reported) != 1 || backend.deleted != 1 { t.Fatalf("reported=%v deleted=%d", tasks.reported, backend.deleted) } // Simulate redelivery after deletion but before the queue ACK. Gitea's // terminal state prevents a duplicate executor from being created. done, err = worker.Handle(context.Background(), assignment()) if err != nil || !done || backend.created != 0 { t.Fatalf("recovery = (%v, %v), created=%d", done, err, backend.created) } } func TestBackendMetadataContainsRecoveryKeys(t *testing.T) { metadata := BackendMetadata(assignment()) if metadata.Labels["ci.ddupan.top/assignment-id"] != "gitea-task-42" { t.Fatalf("labels = %#v", metadata.Labels) } if metadata.Annotations["ci.ddupan.top/spiffe-id"] != "spiffe://ddupan.top/ci/owner/repo/publish" { t.Fatalf("annotations = %#v", metadata.Annotations) } }