| 1 | package jobs |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "io" |
| 6 | "sync" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "reasonix/internal/event" |
| 11 | ) |
| 12 | |
| 13 | // recordingRecorder captures lifecycle calls for assertions. |
| 14 | type recordingRecorder struct { |
| 15 | mu sync.Mutex |
| 16 | starts []string |
| 17 | dones []string |
| 18 | status []Status |
| 19 | } |
| 20 | |
| 21 | func (r *recordingRecorder) RecordStart(id, kind, label string) { |
| 22 | r.mu.Lock() |
| 23 | defer r.mu.Unlock() |
| 24 | r.starts = append(r.starts, id+"|"+kind+"|"+label) |
| 25 | } |
| 26 | |
| 27 | func (r *recordingRecorder) RecordDone(id string, st Status, err error) { |
| 28 | r.mu.Lock() |
| 29 | defer r.mu.Unlock() |
| 30 | r.dones = append(r.dones, id) |
| 31 | r.status = append(r.status, st) |
| 32 | } |
| 33 | |
| 34 | func (r *recordingRecorder) snapshot() (starts, dones []string, status []Status) { |
| 35 | r.mu.Lock() |
| 36 | defer r.mu.Unlock() |
| 37 | return append([]string(nil), r.starts...), append([]string(nil), r.dones...), append([]Status(nil), r.status...) |
| 38 | } |
| 39 | |
| 40 | func TestTaskRecorderHook_StartAndDone(t *testing.T) { |
| 41 | rec := &recordingRecorder{} |
| 42 | m := NewManager(event.Discard, WithTaskRecorder(rec)) |
| 43 | defer m.Close() |
| 44 | |
| 45 | j := m.Start("task", "demo", func(ctx context.Context, out io.Writer) (string, error) { |
| 46 | return "answer", nil |
| 47 | }) |
| 48 | if res := m.Wait(context.Background(), []string{j.ID}, 5); len(res) != 1 || res[0].Status != Done { |
| 49 | t.Fatalf("wait = %+v", res) |
| 50 | } |
| 51 | |
| 52 | starts, dones, status := rec.snapshot() |
| 53 | wantStart := j.ID + "|task|demo" |
| 54 | if len(starts) != 1 || starts[0] != wantStart { |
| 55 | t.Fatalf("starts = %v, want [%s]", starts, wantStart) |
| 56 | } |
| 57 | if len(dones) != 1 || dones[0] != j.ID || len(status) != 1 || status[0] != Done { |
| 58 | t.Fatalf("dones = %v status = %v, want [%s] [done]", dones, status, j.ID) |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | func TestTaskRecorderHook_Failed(t *testing.T) { |
| 63 | rec := &recordingRecorder{} |
| 64 | m := NewManager(event.Discard, WithTaskRecorder(rec)) |
| 65 | defer m.Close() |
| 66 | |
| 67 | j := m.Start("bash", "", func(ctx context.Context, out io.Writer) (string, error) { |
| 68 | return "", context.DeadlineExceeded |
| 69 | }) |
| 70 | if res := m.Wait(context.Background(), []string{j.ID}, 5); len(res) != 1 || res[0].Status != Failed { |
| 71 | t.Fatalf("wait = %+v", res) |
| 72 | } |
| 73 | |
| 74 | _, dones, status := rec.snapshot() |
| 75 | if len(dones) != 1 || status[0] != Failed { |
| 76 | t.Fatalf("dones = %v status = %v, want [%s] [failed]", dones, status, j.ID) |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | func TestTaskRecorderHook_Killed(t *testing.T) { |
| 81 | rec := &recordingRecorder{} |
| 82 | m := NewManager(event.Discard, WithTaskRecorder(rec)) |
| 83 | defer m.Close() |
| 84 | |
| 85 | block := make(chan struct{}) |
| 86 | j := m.Start("task", "", func(ctx context.Context, out io.Writer) (string, error) { |
| 87 | <-block // hang until killed |
| 88 | return "", nil |
| 89 | }) |
| 90 | time.Sleep(50 * time.Millisecond) |
| 91 | m.Kill(j.ID) |
| 92 | close(block) |
| 93 | m.Wait(context.Background(), []string{j.ID}, 5) |
| 94 | |
| 95 | _, dones, status := rec.snapshot() |
| 96 | if len(dones) != 1 || status[0] != Killed { |
| 97 | t.Fatalf("dones = %v status = %v, want [%s] [killed]", dones, status, j.ID) |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | func TestTaskRecorderHook_SetAfterConstruction(t *testing.T) { |
| 102 | rec := &recordingRecorder{} |
| 103 | m := NewManager(event.Discard) |
| 104 | m.SetTaskRecorder(rec) |
| 105 | defer m.Close() |
| 106 | |
| 107 | j := m.Start("bash", "echo", func(ctx context.Context, out io.Writer) (string, error) { |
| 108 | return "", nil |
| 109 | }) |
| 110 | m.Wait(context.Background(), []string{j.ID}, 5) |
| 111 | |
| 112 | starts, _, _ := rec.snapshot() |
| 113 | if len(starts) != 1 { |
| 114 | t.Fatalf("starts = %v, want 1 call", starts) |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | func TestTaskRecorderHook_NilRecorderIsNoop(t *testing.T) { |
| 119 | m := NewManager(event.Discard, WithTaskRecorder(nil)) |
| 120 | defer m.Close() |
| 121 | |
| 122 | j := m.Start("bash", "echo", func(ctx context.Context, out io.Writer) (string, error) { |
| 123 | return "", nil |
| 124 | }) |
| 125 | if res := m.Wait(context.Background(), []string{j.ID}, 5); len(res) != 1 { |
| 126 | t.Fatalf("wait = %+v", res) |
| 127 | } |
| 128 | } |
| 129 |