Skip to content

Commit fa25657

Browse files
nonebackclaude
andcommitted
test(observer,profiler): add comprehensive tests with 79.3% coverage
- Add 13 observer tests: openSpan, closeSpan, withProfiler/Tracer, integration - Add 6 profiler tests: span merge, subflow skip, concurrent safety Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent abe419c commit fa25657

2 files changed

Lines changed: 443 additions & 0 deletions

File tree

observer_test.go

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
package gotaskflow
2+
3+
import (
4+
"bytes"
5+
"sync"
6+
"testing"
7+
"time"
8+
)
9+
10+
// TestObserverOpenSpan tests that openSpan creates a span with correct attributes.
11+
func TestObserverOpenSpan(t *testing.T) {
12+
obs := newObserver()
13+
obs.withProfiler(newProfiler())
14+
15+
node := &innerNode{
16+
name: "test-task",
17+
Typ: nodeStatic,
18+
dependents: []*innerNode{{name: "dep1"}, {name: "dep2"}},
19+
}
20+
21+
s := obs.openSpan(node, nil)
22+
if s == nil {
23+
t.Fatal("expected non-nil span")
24+
}
25+
if s.extra.name != "test-task" {
26+
t.Errorf("expected name 'test-task', got %q", s.extra.name)
27+
}
28+
if s.extra.typ != nodeStatic {
29+
t.Errorf("expected type nodeStatic, got %q", s.extra.typ)
30+
}
31+
if s.parent != nil {
32+
t.Errorf("expected nil parent, got %v", s.parent)
33+
}
34+
if len(s.dependents) != 2 || s.dependents[0] != "dep1" || s.dependents[1] != "dep2" {
35+
t.Errorf("expected dependents [dep1, dep2], got %v", s.dependents)
36+
}
37+
}
38+
39+
// TestObserverOpenSpanNil tests that openSpan returns nil when no profiler or tracer is set.
40+
func TestObserverOpenSpanNil(t *testing.T) {
41+
obs := newObserver()
42+
43+
node := &innerNode{
44+
name: "test-task",
45+
Typ: nodeStatic,
46+
}
47+
48+
s := obs.openSpan(node, nil)
49+
if s != nil {
50+
t.Errorf("expected nil span when no profiler/tracer, got %v", s)
51+
}
52+
}
53+
54+
// TestObserverCloseSpan tests that closeSpan records the span to profiler.
55+
func TestObserverCloseSpan(t *testing.T) {
56+
obs := newObserver()
57+
p := newProfiler()
58+
obs.withProfiler(p)
59+
60+
node := &innerNode{
61+
name: "test-task",
62+
Typ: nodeStatic,
63+
}
64+
65+
s := obs.openSpan(node, nil)
66+
time.Sleep(1 * time.Millisecond) // ensure some duration
67+
obs.closeSpan(s, true)
68+
69+
if len(p.spans) != 1 {
70+
t.Errorf("expected 1 span in profiler, got %d", len(p.spans))
71+
}
72+
if s.cost <= 0 {
73+
t.Errorf("expected positive cost, got %v", s.cost)
74+
}
75+
}
76+
77+
// TestObserverCloseSpanNotOk tests that closeSpan does not record span when ok=false (panic).
78+
func TestObserverCloseSpanNotOk(t *testing.T) {
79+
obs := newObserver()
80+
p := newProfiler()
81+
obs.withProfiler(p)
82+
83+
node := &innerNode{
84+
name: "test-task",
85+
Typ: nodeStatic,
86+
}
87+
88+
s := obs.openSpan(node, nil)
89+
time.Sleep(1 * time.Millisecond)
90+
obs.closeSpan(s, false) // ok=false means panic occurred
91+
92+
if len(p.spans) != 0 {
93+
t.Errorf("expected 0 spans in profiler (panicked task), got %d", len(p.spans))
94+
}
95+
}
96+
97+
// TestObserverCloseSpanNil tests that closeSpan handles nil span gracefully.
98+
func TestObserverCloseSpanNil(t *testing.T) {
99+
obs := newObserver()
100+
obs.withProfiler(newProfiler())
101+
102+
// Should not panic
103+
obs.closeSpan(nil, true)
104+
}
105+
106+
// TestObserverWithProfiler tests that withProfiler sets the profiler correctly.
107+
func TestObserverWithProfiler(t *testing.T) {
108+
obs := newObserver()
109+
if obs.profiler != nil {
110+
t.Error("expected nil profiler initially")
111+
}
112+
113+
p := newProfiler()
114+
obs.withProfiler(p)
115+
if obs.profiler != p {
116+
t.Error("profiler not set correctly")
117+
}
118+
}
119+
120+
// TestObserverWithTracer tests that withTracer sets the tracer correctly.
121+
func TestObserverWithTracer(t *testing.T) {
122+
obs := newObserver()
123+
if obs.tracer != nil {
124+
t.Error("expected nil tracer initially")
125+
}
126+
127+
tr := newTracer()
128+
obs.withTracer(tr)
129+
if obs.tracer != tr {
130+
t.Error("tracer not set correctly")
131+
}
132+
}
133+
134+
// TestObserverSpanParentChain tests that span parent chain is preserved.
135+
func TestObserverSpanParentChain(t *testing.T) {
136+
obs := newObserver()
137+
obs.withProfiler(newProfiler())
138+
139+
parentNode := &innerNode{name: "parent", Typ: nodeStatic}
140+
childNode := &innerNode{name: "child", Typ: nodeStatic}
141+
142+
parentSpan := obs.openSpan(parentNode, nil)
143+
childSpan := obs.openSpan(childNode, parentSpan)
144+
145+
if childSpan.parent != parentSpan {
146+
t.Error("child span should have parent span set")
147+
}
148+
if parentSpan.parent != nil {
149+
t.Error("parent span should have nil parent")
150+
}
151+
}
152+
153+
// TestObserverWithBothProfilerAndTracer tests that both profiler and tracer work together.
154+
func TestObserverWithBothProfilerAndTracer(t *testing.T) {
155+
obs := newObserver()
156+
p := newProfiler()
157+
tr := newTracer()
158+
obs.withProfiler(p)
159+
obs.withTracer(tr)
160+
161+
node := &innerNode{name: "test-task", Typ: nodeStatic}
162+
s := obs.openSpan(node, nil)
163+
time.Sleep(1 * time.Millisecond)
164+
obs.closeSpan(s, true)
165+
166+
if len(p.spans) != 1 {
167+
t.Errorf("expected 1 span in profiler, got %d", len(p.spans))
168+
}
169+
if len(tr.events) != 1 {
170+
t.Errorf("expected 1 event in tracer, got %d", len(tr.events))
171+
}
172+
}
173+
174+
// TestObserverConcurrent tests concurrent use of observer.
175+
func TestObserverConcurrent(t *testing.T) {
176+
obs := newObserver()
177+
p := newProfiler()
178+
tr := newTracer()
179+
obs.withProfiler(p)
180+
obs.withTracer(tr)
181+
182+
var wg sync.WaitGroup
183+
n := 100
184+
wg.Add(n)
185+
186+
for i := 0; i < n; i++ {
187+
go func(i int) {
188+
defer wg.Done()
189+
node := &innerNode{
190+
name: "task",
191+
Typ: nodeStatic,
192+
}
193+
s := obs.openSpan(node, nil)
194+
time.Sleep(time.Microsecond)
195+
obs.closeSpan(s, true)
196+
}(i)
197+
}
198+
wg.Wait()
199+
200+
// Note: profiler merges by attr, so may have fewer than n spans
201+
// But tracer should have all n events
202+
if len(tr.events) != n {
203+
t.Errorf("expected %d tracer events, got %d", n, len(tr.events))
204+
}
205+
}
206+
207+
// TestObserverIntegrationWithExecutor tests observer works correctly with executor.
208+
func TestObserverIntegrationWithExecutor(t *testing.T) {
209+
tf := NewTaskFlow("test-flow")
210+
a := tf.NewTask("A", func() {})
211+
b := tf.NewTask("B", func() {})
212+
a.Precede(b)
213+
214+
exec := NewExecutor(4, WithProfiler())
215+
exec.Run(tf).Wait()
216+
217+
var buf bytes.Buffer
218+
err := exec.Profile(&buf)
219+
if err != nil {
220+
t.Errorf("unexpected error: %v", err)
221+
}
222+
if buf.Len() == 0 {
223+
t.Error("expected profile output, got empty")
224+
}
225+
}
226+
227+
// TestObserverIntegrationWithTracer tests observer works correctly with tracer.
228+
func TestObserverIntegrationWithTracer(t *testing.T) {
229+
tf := NewTaskFlow("test-flow")
230+
a := tf.NewTask("A", func() {})
231+
b := tf.NewTask("B", func() {})
232+
a.Precede(b)
233+
234+
exec := NewExecutor(4, WithTracer())
235+
exec.Run(tf).Wait()
236+
237+
var buf bytes.Buffer
238+
err := exec.Trace(&buf)
239+
if err != nil {
240+
t.Errorf("unexpected error: %v", err)
241+
}
242+
if buf.Len() == 0 {
243+
t.Error("expected trace output, got empty")
244+
}
245+
}
246+
247+
// TestObserverIntegrationWithBoth tests observer with both profiler and tracer.
248+
func TestObserverIntegrationWithBoth(t *testing.T) {
249+
tf := NewTaskFlow("test-flow")
250+
a := tf.NewTask("A", func() {})
251+
b := tf.NewTask("B", func() {})
252+
a.Precede(b)
253+
254+
exec := NewExecutor(4, WithProfiler(), WithTracer())
255+
exec.Run(tf).Wait()
256+
257+
var profileBuf, traceBuf bytes.Buffer
258+
if err := exec.Profile(&profileBuf); err != nil {
259+
t.Errorf("unexpected profile error: %v", err)
260+
}
261+
if err := exec.Trace(&traceBuf); err != nil {
262+
t.Errorf("unexpected trace error: %v", err)
263+
}
264+
if profileBuf.Len() == 0 {
265+
t.Error("expected profile output")
266+
}
267+
if traceBuf.Len() == 0 {
268+
t.Error("expected trace output")
269+
}
270+
}

0 commit comments

Comments
 (0)