1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package parser2021import (22 "os"23 "reflect"24 "strings"25 "testing"26)2728var cases = []struct {29 name string30 cfg string31 tree []Node32 fail bool33}{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 c1arg294 a_child2 c2arg1 c2arg295 }`,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 `a137 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 a2159 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 c1arg2197 `,198 nil,199 true,200 },201 {202 "single directive with closing brace on children's line",203 `a a1 a2 {204 a_child1 c1arg1 c1arg2205 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 child1284 } }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 boo365 import foo366 }`,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) = bar427 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) = bar442 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 baz457 dir aaa/$(foo)/bbb`,458 nil,459 true,460 },461 {462 "macro expansion, multiple arguments",463 `$(foo) = bar baz464 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 b499 $(foo) 1`,500 nil,501 true,502 },503 {504 "macro expansion, missing =",505 `$(foo) a b506 $(foo) 1`,507 nil,508 true,509 },510 {511 "macro expansion, not on top level",512 `a {513 $(foo) = a b514 }515 $(foo) 1`,516 nil,517 true,518 },519 {520 "macro expansion, nested",521 `$(foo) = a522 $(bar) = $(foo) b523 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) = a538 (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) = a560 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}573574func 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}580581func TestRead(t *testing.T) {582 os.Setenv("TESTING_VARIABLE", "ABCDEF")583 os.Setenv("TESTING_VARIABLE2", "ABC2 DEF2")584585 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 return591 }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 return602 }603 return604 }605606 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}