Skip to content

Commit 9ac6f21

Browse files
committed
test(NullPropagation): add comprehensive tests for all node types
Covers every branch of NullPropagation: all iterator types (union, intersection, arrow, intersection arrow, exclusion, caveat, alias, recursive), the defensive len guards, ID preservation on null output, and leaf/unhandled types that pass through unchanged.
1 parent cda7c3b commit 9ac6f21

1 file changed

Lines changed: 257 additions & 0 deletions

File tree

pkg/query/mutations_test.go

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,263 @@ import (
66
"github.com/stretchr/testify/require"
77
)
88

9+
func TestNullPropagation(t *testing.T) {
10+
live := Outline{Type: FixedIteratorType}
11+
null := Outline{Type: NullIteratorType}
12+
13+
// --- UnionIteratorType ---
14+
15+
t.Run("union: all null → null", func(t *testing.T) {
16+
result := NullPropagation(Outline{Type: UnionIteratorType, SubOutlines: []Outline{null, null}})
17+
require.Equal(t, NullIteratorType, result.Type)
18+
})
19+
20+
t.Run("union: all null (3 children) → null", func(t *testing.T) {
21+
result := NullPropagation(Outline{Type: UnionIteratorType, SubOutlines: []Outline{null, null, null}})
22+
require.Equal(t, NullIteratorType, result.Type)
23+
})
24+
25+
t.Run("union: one live child → unchanged", func(t *testing.T) {
26+
result := NullPropagation(Outline{Type: UnionIteratorType, SubOutlines: []Outline{live, null}})
27+
require.Equal(t, UnionIteratorType, result.Type)
28+
})
29+
30+
t.Run("union: mixed (3 children, 1 live) → unchanged", func(t *testing.T) {
31+
result := NullPropagation(Outline{Type: UnionIteratorType, SubOutlines: []Outline{null, live, null}})
32+
require.Equal(t, UnionIteratorType, result.Type)
33+
})
34+
35+
t.Run("union: all live → unchanged", func(t *testing.T) {
36+
result := NullPropagation(Outline{Type: UnionIteratorType, SubOutlines: []Outline{live, live}})
37+
require.Equal(t, UnionIteratorType, result.Type)
38+
})
39+
40+
t.Run("union: preserves ID when null", func(t *testing.T) {
41+
result := NullPropagation(Outline{Type: UnionIteratorType, ID: 7, SubOutlines: []Outline{null, null}})
42+
require.Equal(t, NullIteratorType, result.Type)
43+
require.Equal(t, OutlineNodeID(7), result.ID)
44+
})
45+
46+
// --- IntersectionIteratorType ---
47+
48+
t.Run("intersection: any null child → null", func(t *testing.T) {
49+
result := NullPropagation(Outline{Type: IntersectionIteratorType, SubOutlines: []Outline{live, null}})
50+
require.Equal(t, NullIteratorType, result.Type)
51+
})
52+
53+
t.Run("intersection: null first child → null", func(t *testing.T) {
54+
result := NullPropagation(Outline{Type: IntersectionIteratorType, SubOutlines: []Outline{null, live}})
55+
require.Equal(t, NullIteratorType, result.Type)
56+
})
57+
58+
t.Run("intersection: all null → null", func(t *testing.T) {
59+
result := NullPropagation(Outline{Type: IntersectionIteratorType, SubOutlines: []Outline{null, null}})
60+
require.Equal(t, NullIteratorType, result.Type)
61+
})
62+
63+
t.Run("intersection: one null in 3 children → null", func(t *testing.T) {
64+
result := NullPropagation(Outline{Type: IntersectionIteratorType, SubOutlines: []Outline{live, null, live}})
65+
require.Equal(t, NullIteratorType, result.Type)
66+
})
67+
68+
t.Run("intersection: all live → unchanged", func(t *testing.T) {
69+
result := NullPropagation(Outline{Type: IntersectionIteratorType, SubOutlines: []Outline{live, live}})
70+
require.Equal(t, IntersectionIteratorType, result.Type)
71+
})
72+
73+
t.Run("intersection: preserves ID when null", func(t *testing.T) {
74+
result := NullPropagation(Outline{Type: IntersectionIteratorType, ID: 8, SubOutlines: []Outline{live, null}})
75+
require.Equal(t, NullIteratorType, result.Type)
76+
require.Equal(t, OutlineNodeID(8), result.ID)
77+
})
78+
79+
// --- ArrowIteratorType ---
80+
81+
t.Run("arrow: null left child → null", func(t *testing.T) {
82+
// Null → B has no sources to traverse, so result is empty.
83+
result := NullPropagation(Outline{Type: ArrowIteratorType, SubOutlines: []Outline{null, live}})
84+
require.Equal(t, NullIteratorType, result.Type)
85+
})
86+
87+
t.Run("arrow: null right child → null", func(t *testing.T) {
88+
// A → Null has no target to reach.
89+
result := NullPropagation(Outline{Type: ArrowIteratorType, SubOutlines: []Outline{live, null}})
90+
require.Equal(t, NullIteratorType, result.Type)
91+
})
92+
93+
t.Run("arrow: both children null → null", func(t *testing.T) {
94+
result := NullPropagation(Outline{Type: ArrowIteratorType, SubOutlines: []Outline{null, null}})
95+
require.Equal(t, NullIteratorType, result.Type)
96+
})
97+
98+
t.Run("arrow: neither child null → unchanged", func(t *testing.T) {
99+
result := NullPropagation(Outline{Type: ArrowIteratorType, SubOutlines: []Outline{live, live}})
100+
require.Equal(t, ArrowIteratorType, result.Type)
101+
})
102+
103+
t.Run("arrow: wrong child count (1) → unchanged (defensive guard)", func(t *testing.T) {
104+
result := NullPropagation(Outline{Type: ArrowIteratorType, SubOutlines: []Outline{null}})
105+
require.Equal(t, ArrowIteratorType, result.Type)
106+
})
107+
108+
t.Run("arrow: preserves ID when null", func(t *testing.T) {
109+
result := NullPropagation(Outline{Type: ArrowIteratorType, ID: 42, SubOutlines: []Outline{null, live}})
110+
require.Equal(t, NullIteratorType, result.Type)
111+
require.Equal(t, OutlineNodeID(42), result.ID)
112+
})
113+
114+
// --- IntersectionArrowIteratorType ---
115+
116+
t.Run("intersection arrow: null left child → null", func(t *testing.T) {
117+
result := NullPropagation(Outline{Type: IntersectionArrowIteratorType, SubOutlines: []Outline{null, live}})
118+
require.Equal(t, NullIteratorType, result.Type)
119+
})
120+
121+
t.Run("intersection arrow: null right child → null", func(t *testing.T) {
122+
result := NullPropagation(Outline{Type: IntersectionArrowIteratorType, SubOutlines: []Outline{live, null}})
123+
require.Equal(t, NullIteratorType, result.Type)
124+
})
125+
126+
t.Run("intersection arrow: both children null → null", func(t *testing.T) {
127+
result := NullPropagation(Outline{Type: IntersectionArrowIteratorType, SubOutlines: []Outline{null, null}})
128+
require.Equal(t, NullIteratorType, result.Type)
129+
})
130+
131+
t.Run("intersection arrow: neither child null → unchanged", func(t *testing.T) {
132+
result := NullPropagation(Outline{Type: IntersectionArrowIteratorType, SubOutlines: []Outline{live, live}})
133+
require.Equal(t, IntersectionArrowIteratorType, result.Type)
134+
})
135+
136+
t.Run("intersection arrow: wrong child count (1) → unchanged (defensive guard)", func(t *testing.T) {
137+
result := NullPropagation(Outline{Type: IntersectionArrowIteratorType, SubOutlines: []Outline{null}})
138+
require.Equal(t, IntersectionArrowIteratorType, result.Type)
139+
})
140+
141+
t.Run("intersection arrow: preserves ID when null", func(t *testing.T) {
142+
result := NullPropagation(Outline{Type: IntersectionArrowIteratorType, ID: 9, SubOutlines: []Outline{null, live}})
143+
require.Equal(t, NullIteratorType, result.Type)
144+
require.Equal(t, OutlineNodeID(9), result.ID)
145+
})
146+
147+
// --- ExclusionIteratorType ---
148+
149+
t.Run("exclusion: null left child (minuend) → null", func(t *testing.T) {
150+
result := NullPropagation(Outline{Type: ExclusionIteratorType, SubOutlines: []Outline{null, live}})
151+
require.Equal(t, NullIteratorType, result.Type)
152+
})
153+
154+
t.Run("exclusion: null right child (subtrahend) → unchanged (A − ∅ = A)", func(t *testing.T) {
155+
// Subtracting the empty set is a no-op; the node stays for a later pass.
156+
result := NullPropagation(Outline{Type: ExclusionIteratorType, SubOutlines: []Outline{live, null}})
157+
require.Equal(t, ExclusionIteratorType, result.Type)
158+
})
159+
160+
t.Run("exclusion: both null → null (left child triggers)", func(t *testing.T) {
161+
result := NullPropagation(Outline{Type: ExclusionIteratorType, SubOutlines: []Outline{null, null}})
162+
require.Equal(t, NullIteratorType, result.Type)
163+
})
164+
165+
t.Run("exclusion: neither null → unchanged", func(t *testing.T) {
166+
result := NullPropagation(Outline{Type: ExclusionIteratorType, SubOutlines: []Outline{live, live}})
167+
require.Equal(t, ExclusionIteratorType, result.Type)
168+
})
169+
170+
t.Run("exclusion: wrong child count (1) → unchanged (defensive guard)", func(t *testing.T) {
171+
result := NullPropagation(Outline{Type: ExclusionIteratorType, SubOutlines: []Outline{null}})
172+
require.Equal(t, ExclusionIteratorType, result.Type)
173+
})
174+
175+
t.Run("exclusion: preserves ID when null", func(t *testing.T) {
176+
result := NullPropagation(Outline{Type: ExclusionIteratorType, ID: 5, SubOutlines: []Outline{null, live}})
177+
require.Equal(t, NullIteratorType, result.Type)
178+
require.Equal(t, OutlineNodeID(5), result.ID)
179+
})
180+
181+
// --- CaveatIteratorType ---
182+
183+
t.Run("caveat: null child → null", func(t *testing.T) {
184+
result := NullPropagation(Outline{Type: CaveatIteratorType, SubOutlines: []Outline{null}})
185+
require.Equal(t, NullIteratorType, result.Type)
186+
})
187+
188+
t.Run("caveat: live child → unchanged", func(t *testing.T) {
189+
result := NullPropagation(Outline{Type: CaveatIteratorType, SubOutlines: []Outline{live}})
190+
require.Equal(t, CaveatIteratorType, result.Type)
191+
})
192+
193+
t.Run("caveat: wrong child count (2) → unchanged (defensive guard)", func(t *testing.T) {
194+
result := NullPropagation(Outline{Type: CaveatIteratorType, SubOutlines: []Outline{null, null}})
195+
require.Equal(t, CaveatIteratorType, result.Type)
196+
})
197+
198+
t.Run("caveat: preserves ID when null", func(t *testing.T) {
199+
result := NullPropagation(Outline{Type: CaveatIteratorType, ID: 3, SubOutlines: []Outline{null}})
200+
require.Equal(t, NullIteratorType, result.Type)
201+
require.Equal(t, OutlineNodeID(3), result.ID)
202+
})
203+
204+
// --- AliasIteratorType ---
205+
206+
t.Run("alias: null child → null", func(t *testing.T) {
207+
result := NullPropagation(Outline{Type: AliasIteratorType, SubOutlines: []Outline{null}})
208+
require.Equal(t, NullIteratorType, result.Type)
209+
})
210+
211+
t.Run("alias: live child → unchanged", func(t *testing.T) {
212+
result := NullPropagation(Outline{Type: AliasIteratorType, SubOutlines: []Outline{live}})
213+
require.Equal(t, AliasIteratorType, result.Type)
214+
})
215+
216+
t.Run("alias: wrong child count (2) → unchanged (defensive guard)", func(t *testing.T) {
217+
result := NullPropagation(Outline{Type: AliasIteratorType, SubOutlines: []Outline{null, null}})
218+
require.Equal(t, AliasIteratorType, result.Type)
219+
})
220+
221+
// --- RecursiveIteratorType ---
222+
223+
t.Run("recursive: null child → null", func(t *testing.T) {
224+
result := NullPropagation(Outline{Type: RecursiveIteratorType, SubOutlines: []Outline{null}})
225+
require.Equal(t, NullIteratorType, result.Type)
226+
})
227+
228+
t.Run("recursive: live child → unchanged", func(t *testing.T) {
229+
result := NullPropagation(Outline{Type: RecursiveIteratorType, SubOutlines: []Outline{live}})
230+
require.Equal(t, RecursiveIteratorType, result.Type)
231+
})
232+
233+
t.Run("recursive: wrong child count (2) → unchanged (defensive guard)", func(t *testing.T) {
234+
result := NullPropagation(Outline{Type: RecursiveIteratorType, SubOutlines: []Outline{null, null}})
235+
require.Equal(t, RecursiveIteratorType, result.Type)
236+
})
237+
238+
// --- Leaf / unhandled types → always unchanged ---
239+
240+
t.Run("null node itself → unchanged", func(t *testing.T) {
241+
result := NullPropagation(Outline{Type: NullIteratorType})
242+
require.Equal(t, NullIteratorType, result.Type)
243+
})
244+
245+
t.Run("datastore leaf → unchanged", func(t *testing.T) {
246+
result := NullPropagation(Outline{Type: DatastoreIteratorType})
247+
require.Equal(t, DatastoreIteratorType, result.Type)
248+
})
249+
250+
t.Run("self leaf → unchanged", func(t *testing.T) {
251+
result := NullPropagation(Outline{Type: SelfIteratorType})
252+
require.Equal(t, SelfIteratorType, result.Type)
253+
})
254+
255+
t.Run("fixed leaf → unchanged", func(t *testing.T) {
256+
result := NullPropagation(Outline{Type: FixedIteratorType})
257+
require.Equal(t, FixedIteratorType, result.Type)
258+
})
259+
260+
t.Run("recursive sentinel leaf → unchanged", func(t *testing.T) {
261+
result := NullPropagation(Outline{Type: RecursiveSentinelIteratorType})
262+
require.Equal(t, RecursiveSentinelIteratorType, result.Type)
263+
})
264+
}
265+
9266
func TestReorderMutation(t *testing.T) {
10267
t.Run("reorders children correctly", func(t *testing.T) {
11268
child0 := Outline{Type: FixedIteratorType, Args: &IteratorArgs{FixedPaths: []Path{*MustPathFromString("document:doc0#viewer@user:alice")}}}

0 commit comments

Comments
 (0)