@@ -2,13 +2,21 @@ package formatter
22
33import (
44 "bytes"
5+ "context"
56 "fmt"
67 "strings"
78 "testing"
89
10+ "github.com/antlr4-go/antlr/v4"
11+
912 "github.com/MontFerret/ferret/v2/pkg/compiler"
13+ "github.com/MontFerret/ferret/v2/pkg/parser"
14+ "github.com/MontFerret/ferret/v2/pkg/parser/fql"
15+ "github.com/MontFerret/ferret/v2/pkg/runtime"
1016 "github.com/MontFerret/ferret/v2/pkg/source"
17+ "github.com/MontFerret/ferret/v2/pkg/vm"
1118 "github.com/MontFerret/ferret/v2/test/spec"
19+ "github.com/MontFerret/ferret/v2/test/spec/mock"
1220)
1321
1422func TestFormatterSimplifiesOnlyRedundantParentheses (t * testing.T ) {
@@ -71,6 +79,257 @@ func TestFormatterSimplifiesOnlyRedundantParentheses(t *testing.T) {
7179 }
7280}
7381
82+ func TestFormatterWaitForEventOperands (t * testing.T ) {
83+ tests := []struct {
84+ name string
85+ input string
86+ want string
87+ wantName string
88+ wantSource string
89+ }{
90+ {
91+ name : "composed operands" ,
92+ input : `RETURN WAITFOR EVENT (@kind ?? "message") IN (@source ?? fallback)` ,
93+ want : `return waitfor event @kind ?? "message" in @source ?? fallback` ,
94+ wantName : `@kind??"message"` ,
95+ wantSource : `@source??fallback` ,
96+ },
97+ {
98+ name : "membership name removes redundant grouping" ,
99+ input : `RETURN WAITFOR EVENT (@kind IN @names) IN @source` ,
100+ want : `return waitfor event @kind in @names in @source` ,
101+ wantName : `@kindin@names` ,
102+ wantSource : `@source` ,
103+ },
104+ {
105+ name : "membership source retains delimiter boundary" ,
106+ input : `RETURN WAITFOR EVENT "message" IN (@candidate IN @sources)` ,
107+ want : `return waitfor event "message" in (@candidate in @sources)` ,
108+ wantName : `"message"` ,
109+ wantSource : `(@candidatein@sources)` ,
110+ },
111+ {
112+ name : "negated membership source retains delimiter boundary" ,
113+ input : `RETURN WAITFOR EVENT "message" IN (@candidate NOT IN @sources)` ,
114+ want : `return waitfor event "message" in (@candidate not in @sources)` ,
115+ wantName : `"message"` ,
116+ wantSource : `(@candidatenotin@sources)` ,
117+ },
118+ {
119+ name : "function argument membership is bounded" ,
120+ input : `RETURN WAITFOR EVENT "message" IN (SOURCE(@candidate IN @sources))` ,
121+ want : `return waitfor event "message" in SOURCE(@candidate in @sources)` ,
122+ wantName : `"message"` ,
123+ wantSource : `SOURCE(@candidatein@sources)` ,
124+ },
125+ {
126+ name : "array entry membership is bounded" ,
127+ input : `RETURN WAITFOR EVENT "message" IN ([@candidate IN @sources])` ,
128+ want : `return waitfor event "message" in [@candidate in @sources]` ,
129+ wantName : `"message"` ,
130+ wantSource : `[@candidatein@sources]` ,
131+ },
132+ {
133+ name : "object property membership is bounded" ,
134+ input : `RETURN WAITFOR EVENT "message" IN ({ candidate: @candidate IN @sources })` ,
135+ want : `return waitfor event "message" in { candidate: @candidate in @sources }` ,
136+ wantName : `"message"` ,
137+ wantSource : `{candidate:@candidatein@sources}` ,
138+ },
139+ {
140+ name : "ternary true branch membership is bounded" ,
141+ input : `RETURN WAITFOR EVENT "message" IN (TRUE ? @candidate IN @sources : FALSE)` ,
142+ want : `return waitfor event "message" in true ? @candidate in @sources : false` ,
143+ wantName : `"message"` ,
144+ wantSource : `true?@candidatein@sources:false` ,
145+ },
146+ {
147+ name : "precedence grouping bounds nested membership" ,
148+ input : `RETURN WAITFOR EVENT "message" IN ((@candidate IN @sources) + 1)` ,
149+ want : `return waitfor event "message" in (@candidate in @sources) + 1` ,
150+ wantName : `"message"` ,
151+ wantSource : `(@candidatein@sources)+1` ,
152+ },
153+ {
154+ name : "removable inner grouping leaves membership exposed" ,
155+ input : `RETURN WAITFOR EVENT "message" IN (NOT (@candidate IN @sources))` ,
156+ want : `return waitfor event "message" in (not @candidate in @sources)` ,
157+ wantName : `"message"` ,
158+ wantSource : `(not@candidatein@sources)` ,
159+ },
160+ {
161+ name : "ternary condition membership remains exposed" ,
162+ input : `RETURN WAITFOR EVENT "message" IN (@candidate IN @sources ? TRUE : FALSE)` ,
163+ want : `return waitfor event "message" in (@candidate in @sources ? true : false)` ,
164+ wantName : `"message"` ,
165+ wantSource : `(@candidatein@sources?true:false)` ,
166+ },
167+ {
168+ name : "ternary false branch membership remains exposed" ,
169+ input : `RETURN WAITFOR EVENT "message" IN (TRUE ? FALSE : @candidate IN @sources)` ,
170+ want : `return waitfor event "message" in (true ? false : @candidate in @sources)` ,
171+ wantName : `"message"` ,
172+ wantSource : `(true?false:@candidatein@sources)` ,
173+ },
174+ {
175+ name : "membership operands use distinct grouping" ,
176+ input : `RETURN WAITFOR EVENT (@kind IN @names) IN (@candidate IN @sources)` ,
177+ want : `return waitfor event @kind in @names in (@candidate in @sources)` ,
178+ wantName : `@kindin@names` ,
179+ wantSource : `(@candidatein@sources)` ,
180+ },
181+ {
182+ name : "array membership source retains delimiter boundary" ,
183+ input : `RETURN WAITFOR EVENT "message" IN (@candidate ANY IN @sources)` ,
184+ want : `return waitfor event "message" in (@candidate any in @sources)` ,
185+ wantName : `"message"` ,
186+ wantSource : `(@candidateanyin@sources)` ,
187+ },
188+ {
189+ name : "query source" ,
190+ input : `RETURN WAITFOR EVENT GET_NAME(@id) IN QUERY ONE ".source" IN @registry` ,
191+ want : `return waitfor event GET_NAME(@id) in query one ".source" in @registry` ,
192+ wantName : `GET_NAME(@id)` ,
193+ wantSource : `queryone".source"in@registry` ,
194+ },
195+ }
196+
197+ for _ , test := range tests {
198+ t .Run (test .name , func (t * testing.T ) {
199+ got := formatParenthesesStable (t , test .input )
200+ if got != test .want {
201+ t .Fatalf ("formatted output:\n %s\n want:\n %s" , got , test .want )
202+ }
203+
204+ assertWaitForEventOperandBoundaries (t , got , test .wantName , test .wantSource )
205+ })
206+ }
207+ }
208+
209+ func assertWaitForEventOperandBoundaries (t * testing.T , input , wantName , wantSource string ) {
210+ t .Helper ()
211+
212+ p := parser .New (input )
213+ program := p .Program ()
214+ if ! p .AtEOF () {
215+ t .Fatalf ("formatted output did not parse completely:\n %s" , input )
216+ }
217+
218+ event := findFirstWaitForEvent (program )
219+ if event == nil {
220+ t .Fatalf ("formatted output has no WAITFOR EVENT expression:\n %s" , input )
221+ }
222+
223+ name := event .WaitForEventName ().(* fql.WaitForEventNameContext ).Expression ()
224+ if got := name .GetText (); got != wantName {
225+ t .Fatalf ("reparsed event-name expression = %q, want %q" , got , wantName )
226+ }
227+
228+ source := event .WaitForEventSource ().(* fql.WaitForEventSourceContext ).Expression ()
229+ if got := source .GetText (); got != wantSource {
230+ t .Fatalf ("reparsed source expression = %q, want %q" , got , wantSource )
231+ }
232+ }
233+
234+ func findFirstWaitForEvent (tree antlr.Tree ) * fql.WaitForEventExpressionContext {
235+ if tree == nil {
236+ return nil
237+ }
238+
239+ if event , ok := tree .(* fql.WaitForEventExpressionContext ); ok {
240+ return event
241+ }
242+
243+ for i := 0 ; i < tree .GetChildCount (); i ++ {
244+ if event := findFirstWaitForEvent (tree .GetChild (i )); event != nil {
245+ return event
246+ }
247+ }
248+
249+ return nil
250+ }
251+
252+ func TestFormatterWaitForEventOperandsPreserveExecution (t * testing.T ) {
253+ tests := []struct {
254+ newEnvironment func () []vm.EnvironmentOption
255+ name string
256+ input string
257+ }{
258+ {
259+ name : "composed operands" ,
260+ input : `RETURN WAITFOR EVENT (@eventName ?? "message") IN (@source ?? @fallback)
261+ WHEN .type == "match"` ,
262+ newEnvironment : func () []vm.EnvironmentOption {
263+ source := mock .NewObservable ([]runtime.Value {
264+ mock .NewTestEventType ("ignored" ),
265+ mock .NewTestEventType ("match" ),
266+ })
267+
268+ return []vm.EnvironmentOption {
269+ vm .WithParams (map [string ]runtime.Value {
270+ "eventName" : runtime .None ,
271+ "fallback" : runtime .None ,
272+ "source" : source ,
273+ }),
274+ }
275+ },
276+ },
277+ {
278+ name : "bounded membership source" ,
279+ input : `RETURN WAITFOR EVENT "message" IN (SOURCE(@candidate IN @sources))
280+ WHEN .type == "match"` ,
281+ newEnvironment : func () []vm.EnvironmentOption {
282+ source := mock .NewObservable ([]runtime.Value {
283+ mock .NewTestEventType ("ignored" ),
284+ mock .NewTestEventType ("match" ),
285+ })
286+
287+ return []vm.EnvironmentOption {
288+ vm .WithParams (map [string ]runtime.Value {
289+ "candidate" : runtime .NewString ("candidate" ),
290+ "sources" : runtime .NewArrayWith (runtime .NewString ("candidate" )),
291+ }),
292+ vm .WithFunction ("SOURCE" , func (context.Context , ... runtime.Value ) (runtime.Value , error ) {
293+ return source , nil
294+ }),
295+ }
296+ },
297+ },
298+ }
299+
300+ for _ , test := range tests {
301+ formatted := formatParenthesesStable (t , test .input )
302+
303+ for _ , level := range []compiler.OptimizationLevel {compiler .O0 , compiler .O1 } {
304+ t .Run (test .name + "/" + optimizationNameForFormatter (level ), func (t * testing.T ) {
305+ originalProgram , err := spec .Compile (test .input , level )
306+ if err != nil {
307+ t .Fatalf ("compile original: %v" , err )
308+ }
309+
310+ formattedProgram , err := spec .Compile (formatted , level )
311+ if err != nil {
312+ t .Fatalf ("compile formatted: %v\n %s" , err , formatted )
313+ }
314+
315+ originalResult , err := spec .Run (originalProgram , test .newEnvironment ()... )
316+ if err != nil {
317+ t .Fatalf ("run original: %v" , err )
318+ }
319+
320+ formattedResult , err := spec .Run (formattedProgram , test .newEnvironment ()... )
321+ if err != nil {
322+ t .Fatalf ("run formatted: %v" , err )
323+ }
324+
325+ if ! bytes .Equal (originalResult , formattedResult ) {
326+ t .Fatalf ("execution changed: original %s, formatted %s\n %s" , originalResult , formattedResult , formatted )
327+ }
328+ })
329+ }
330+ }
331+ }
332+
74333func TestFormatterSimplifiesGroupedForOnlyAtStatementBoundaries (t * testing.T ) {
75334 tests := []struct {
76335 name string
0 commit comments