maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package parser
 20
 21import (
 22	"os"
 23	"reflect"
 24	"strings"
 25	"testing"
 26)
 27
 28var cases = []struct {
 29	name string
 30	cfg  string
 31	tree []Node
 32	fail bool
 33}{
 34	{
 35		"single directive without args",
 36		`a`,
 37		[]Node{
 38			{
 39				Name:     "a",
 40				Args:     []string{},
 41				Children: nil,
 42				File:     "test",
 43				Line:     1,
 44			},
 45		},
 46		false,
 47	},
 48	{
 49		"single directive with args",
 50		`a a1 a2`,
 51		[]Node{
 52			{
 53				Name:     "a",
 54				Args:     []string{"a1", "a2"},
 55				Children: nil,
 56				File:     "test",
 57				Line:     1,
 58			},
 59		},
 60		false,
 61	},
 62	{
 63		"single directive with empty braces",
 64		`a { }`,
 65		[]Node{
 66			{
 67				Name:     "a",
 68				Args:     []string{},
 69				Children: []Node{},
 70				File:     "test",
 71				Line:     1,
 72			},
 73		},
 74		false,
 75	},
 76	{
 77		"single directive with arguments and empty braces",
 78		`a a1 a2 { }`,
 79		[]Node{
 80			{
 81				Name:     "a",
 82				Args:     []string{"a1", "a2"},
 83				Children: []Node{},
 84				File:     "test",
 85				Line:     1,
 86			},
 87		},
 88		false,
 89	},
 90	{
 91		"single directive with a block",
 92		`a a1 a2 {
 93			a_child1 c1arg1 c1arg2
 94			a_child2 c2arg1 c2arg2
 95		}`,
 96		[]Node{
 97			{
 98				Name: "a",
 99				Args: []string{"a1", "a2"},
100				Children: []Node{
101					{
102						Name:     "a_child1",
103						Args:     []string{"c1arg1", "c1arg2"},
104						Children: nil,
105						File:     "test",
106						Line:     2,
107					},
108					{
109						Name:     "a_child2",
110						Args:     []string{"c2arg1", "c2arg2"},
111						Children: nil,
112						File:     "test",
113						Line:     3,
114					},
115				},
116				File: "test",
117				Line: 1,
118			},
119		},
120		false,
121	},
122	{
123		"single directive with missing closing brace",
124		`a {`,
125		nil,
126		true,
127	},
128	{
129		"single directive with missing opening brace",
130		`a }`,
131		nil,
132		true,
133	},
134	{
135		"two directives",
136		`a
137		 b`,
138		[]Node{
139			{
140				Name:     "a",
141				Args:     []string{},
142				Children: nil,
143				File:     "test",
144				Line:     1,
145			},
146			{
147				Name:     "b",
148				Args:     []string{},
149				Children: nil,
150				File:     "test",
151				Line:     2,
152			},
153		},
154		false,
155	},
156	{
157		"two directives with arguments",
158		`a a1 a2
159		 b b1 b2`,
160		[]Node{
161			{
162				Name:     "a",
163				Args:     []string{"a1", "a2"},
164				Children: nil,
165				File:     "test",
166				Line:     1,
167			},
168			{
169				Name:     "b",
170				Args:     []string{"b1", "b2"},
171				Children: nil,
172				File:     "test",
173				Line:     2,
174			},
175		},
176		false,
177	},
178	{
179		"backslash on the end of line",
180		`a a1 a2 \
181		   a3 a4`,
182		[]Node{
183			{
184				Name:     "a",
185				Args:     []string{"a1", "a2", "a3", "a4"},
186				Children: nil,
187				File:     "test",
188				Line:     1,
189			},
190		},
191		false,
192	},
193	{
194		"directive with missing closing brace on different line",
195		`a a1 a2 {
196			a_child1 c1arg1 c1arg2
197		`,
198		nil,
199		true,
200	},
201	{
202		"single directive with closing brace on children's line",
203		`a a1 a2 {
204			a_child1 c1arg1 c1arg2
205			a_child2 c2arg1 c2arg2 }
206		 b`,
207		[]Node{
208			{
209				Name: "a",
210				Args: []string{"a1", "a2"},
211				Children: []Node{
212					{
213						Name:     "a_child1",
214						Args:     []string{"c1arg1", "c1arg2"},
215						Children: nil,
216						File:     "test",
217						Line:     2,
218					},
219					{
220						Name:     "a_child2",
221						Args:     []string{"c2arg1", "c2arg2"},
222						Children: nil,
223						File:     "test",
224						Line:     3,
225					},
226				},
227				File: "test",
228				Line: 1,
229			},
230			{
231				Name:     "b",
232				Args:     []string{},
233				Children: nil,
234				File:     "test",
235				Line:     4,
236			},
237		},
238		false,
239	},
240	{
241		"single directive with childrens on the same line",
242		`a a1 a2 { a_child1 c1arg1 c1arg2 }`,
243		[]Node{
244			{
245				Name: "a",
246				Args: []string{"a1", "a2"},
247				Children: []Node{
248					{
249						Name:     "a_child1",
250						Args:     []string{"c1arg1", "c1arg2"},
251						Children: nil,
252						File:     "test",
253						Line:     1,
254					},
255				},
256				File: "test",
257				Line: 1,
258			},
259		},
260		false,
261	},
262	{
263		"invalid directive name",
264		`a-a4@%8 whatever`,
265		nil,
266		true,
267	},
268	{
269		"directive name starts with a digit",
270		`1w whatever`,
271		nil,
272		true,
273	},
274	{
275		"missing block header",
276		`{ a_child1 c1arg1 c1arg2 }`,
277		nil,
278		true,
279	},
280	{
281		"extra closing brace",
282		`a {
283			child1
284		} }
285		`,
286		nil,
287		true,
288	},
289	{
290		"extra opening brace",
291		`a { {
292		}`,
293		nil,
294		true,
295	},
296	{
297		"closing brace in next block header",
298		`a {
299		} b b1`,
300		nil,
301		true,
302	},
303	{
304		"environment variable expansion",
305		`a {env:TESTING_VARIABLE}`,
306		[]Node{
307			{
308				Name:     "a",
309				Args:     []string{"ABCDEF"},
310				Children: nil,
311				File:     "test",
312				Line:     1,
313			},
314		},
315		false,
316	},
317	{
318		"missing environment variable expansion (unix-like syntax)",
319		`a {env:TESTING_VARIABLE3}`,
320		[]Node{
321			{
322				Name:     "a",
323				Args:     []string{""},
324				Children: nil,
325				File:     "test",
326				Line:     1,
327			},
328		},
329		false,
330	},
331	{
332		"incomplete environment variable syntax",
333		`a {env:TESTING_VARIABLE`,
334		[]Node{
335			{
336				Name:     "a",
337				Args:     []string{"{env:TESTING_VARIABLE"},
338				Children: nil,
339				File:     "test",
340				Line:     1,
341			},
342		},
343		false,
344	},
345	{
346		"snippet expansion",
347		`(foo) { a }
348		 import foo`,
349		[]Node{
350			{
351				Name:     "a",
352				Args:     []string{},
353				Children: nil,
354				File:     "test",
355				Line:     1,
356			},
357		},
358		false,
359	},
360	{
361		"snippet expansion inside a block",
362		`(foo) { a }
363        foo {
364            boo
365            import foo
366        }`,
367		[]Node{
368			{
369				Name: "foo",
370				Args: []string{},
371				Children: []Node{
372					{
373						Name: "boo",
374						Args: []string{},
375						File: "test",
376						Line: 3,
377					},
378					{
379						Name: "a",
380						Args: []string{},
381						File: "test",
382						Line: 1,
383					},
384				},
385				File: "test",
386				Line: 2,
387			},
388		},
389		false,
390	},
391	{
392		"missing snippet",
393		`import foo`,
394		nil,
395		true,
396	},
397	{
398		"unlimited recursive snippet expansion",
399		`(foo) { import foo }
400		 import foo`,
401		nil,
402		true,
403	},
404	{
405		"snippet declaration with args",
406		`(foo) a b c { }`,
407		nil,
408		true,
409	},
410	{
411		"snippet declaration inside block",
412		`abc {
413			(foo) { }
414		}`,
415		nil,
416		true,
417	},
418	{
419		"block nesting limit",
420		`a ` + strings.Repeat("a { ", 1000) + strings.Repeat(" }", 1000),
421		nil,
422		true,
423	},
424	{
425		"macro expansion, single argument",
426		`$(foo) = bar
427		dir $(foo)`,
428		[]Node{
429			{
430				Name:     "dir",
431				Args:     []string{"bar"},
432				Children: nil,
433				File:     "test",
434				Line:     2,
435			},
436		},
437		false,
438	},
439	{
440		"macro expansion, inside argument",
441		`$(foo) = bar
442		dir aaa/$(foo)/bbb`,
443		[]Node{
444			{
445				Name:     "dir",
446				Args:     []string{"aaa/bar/bbb"},
447				Children: nil,
448				File:     "test",
449				Line:     2,
450			},
451		},
452		false,
453	},
454	{
455		"macro expansion, inside argument, multi-value",
456		`$(foo) = bar baz
457		dir aaa/$(foo)/bbb`,
458		nil,
459		true,
460	},
461	{
462		"macro expansion, multiple arguments",
463		`$(foo) = bar baz
464		dir $(foo)`,
465		[]Node{
466			{
467				Name:     "dir",
468				Args:     []string{"bar", "baz"},
469				Children: nil,
470				File:     "test",
471				Line:     2,
472			},
473		},
474		false,
475	},
476	{
477		"macro expansion, undefined",
478		`dir $(foo)`,
479		[]Node{
480			{
481				Name:     "dir",
482				Args:     []string{},
483				Children: nil,
484				File:     "test",
485				Line:     1,
486			},
487		},
488		false,
489	},
490	{
491		"macro expansion, empty",
492		`$(foo) =`,
493		nil,
494		true,
495	},
496	{
497		"macro expansion, name replacement",
498		`$(foo) = a b
499			$(foo) 1`,
500		nil,
501		true,
502	},
503	{
504		"macro expansion, missing =",
505		`$(foo) a b
506			$(foo) 1`,
507		nil,
508		true,
509	},
510	{
511		"macro expansion, not on top level",
512		`a {
513				$(foo) = a b
514			}
515			$(foo) 1`,
516		nil,
517		true,
518	},
519	{
520		"macro expansion, nested",
521		`$(foo) = a
522			$(bar) = $(foo) b
523			dir $(bar)`,
524		[]Node{
525			{
526				Name:     "dir",
527				Args:     []string{"a", "b"},
528				Children: nil,
529				File:     "test",
530				Line:     3,
531			},
532		},
533		false,
534	},
535	{
536		"macro expansion, used inside snippet",
537		`$(foo) = a
538			(bar) {
539				dir $(foo)
540			}
541			import bar`,
542		[]Node{
543			{
544				Name:     "dir",
545				Args:     []string{"a"},
546				Children: nil,
547				File:     "test",
548				Line:     3,
549			},
550		},
551		false,
552	},
553	{
554		"macro expansion, used inside snippet, defined after",
555		`
556			(bar) {
557				dir $(foo)
558			}
559			$(foo) = a
560			import bar`,
561		[]Node{
562			{
563				Name:     "dir",
564				Args:     []string{},
565				Children: nil,
566				File:     "test",
567				Line:     3,
568			},
569		},
570		false,
571	},
572}
573
574func printTree(t *testing.T, root Node, indent int) {
575	t.Log(strings.Repeat(" ", indent)+root.Name, root.Args)
576	for _, child := range root.Children {
577		t.Log(child, indent+1)
578	}
579}
580
581func TestRead(t *testing.T) {
582	os.Setenv("TESTING_VARIABLE", "ABCDEF")
583	os.Setenv("TESTING_VARIABLE2", "ABC2 DEF2")
584
585	for _, case_ := range cases {
586		t.Run(case_.name, func(t *testing.T) {
587			tree, err := Read(strings.NewReader(case_.cfg), "test")
588			if !case_.fail && err != nil {
589				t.Error("unexpected failure:", err)
590				return
591			}
592			if case_.fail {
593				if err == nil {
594					t.Log("expected failure but Read succeeded")
595					t.Log("got tree:")
596					t.Logf("%+v", tree)
597					for _, node := range tree {
598						printTree(t, node, 0)
599					}
600					t.Fail()
601					return
602				}
603				return
604			}
605
606			if !reflect.DeepEqual(case_.tree, tree) {
607				t.Log("parse result mismatch")
608				t.Log("expected:")
609				t.Logf("%+#v", case_.tree)
610				for _, node := range case_.tree {
611					printTree(t, node, 0)
612				}
613				t.Log("actual:")
614				t.Logf("%+#v", tree)
615				for _, node := range tree {
616					printTree(t, node, 0)
617				}
618				t.Fail()
619			}
620		})
621	}
622}