github.com/CiscoM31/godata@v1.0.10/expression_parser_test.go (about) 1 package godata 2 3 import ( 4 "context" 5 "fmt" 6 "regexp" 7 "strings" 8 "testing" 9 ) 10 11 func TestTokenTypes(t *testing.T) { 12 if expressionTokenLast.String() != "expressionTokenLast" { 13 t.Errorf("Unexpected String() value: %v", expressionTokenLast) 14 } 15 } 16 17 func TestExpressionDateTime(t *testing.T) { 18 ctx := context.Background() 19 tokenizer := NewExpressionTokenizer() 20 tokens := map[string]ExpressionTokenType{ 21 "2011-08-29T21:58Z": ExpressionTokenDateTime, 22 "2011-08-29T21:58:33Z": ExpressionTokenDateTime, 23 "2011-08-29T21:58:33.123Z": ExpressionTokenDateTime, 24 "2011-08-29T21:58+11:23": ExpressionTokenDateTime, 25 "2011-08-29T21:58:33+11:23": ExpressionTokenDateTime, 26 "2011-08-29T21:58:33.123+11:23": ExpressionTokenDateTime, 27 "2011-08-29T21:58:33-11:23": ExpressionTokenDateTime, 28 "2011-08-29": ExpressionTokenDate, 29 "21:58:33": ExpressionTokenTime, 30 } 31 for tokenValue, tokenType := range tokens { 32 // Previously, the unit test had no space character after 'gt' 33 // E.g. 'CreateTime gt2011-08-29T21:58Z' was considered valid. 34 // However the ABNF notation for ODATA logical operators is: 35 // gtExpr = RWS "gt" RWS commonExpr 36 // RWS = 1*( SP / HTAB / "%20" / "%09" ) ; "required" whitespace 37 // 38 // See http://docs.oasis-open.org/odata/odata/v4.01/csprd03/abnf/odata-abnf-construction-rules.txt 39 input := "CreateTime gt " + tokenValue 40 expect := []*Token{ 41 {Value: "CreateTime", Type: ExpressionTokenLiteral}, 42 {Value: "gt", Type: ExpressionTokenLogical}, 43 {Value: tokenValue, Type: tokenType}, 44 } 45 output, err := tokenizer.Tokenize(ctx, input) 46 if err != nil { 47 t.Errorf("Failed to tokenize input %s. Error: %v", input, err) 48 } 49 50 result, err := CompareTokens(expect, output) 51 if !result { 52 var a []string 53 for _, t := range output { 54 a = append(a, t.Value) 55 } 56 57 t.Errorf("Unexpected tokens for input '%s'. Tokens: %s Error: %v", input, strings.Join(a, ", "), err) 58 } 59 } 60 } 61 62 func TestValidBooleanExpressionSyntax(t *testing.T) { 63 queries := []string{ 64 "substring(CompanyName,1,2) eq 'lf'", // substring with 3 arguments. 65 // Bolean values 66 "true", 67 "false", 68 "(true)", 69 "((true))", 70 "((true)) or false", 71 "not true", 72 "not false", 73 "not (not true)", 74 // TODO: this should work because 'not' is inherently right-associative. 75 // I.e. it should be interpreted as not (not true) 76 // If it were left-associative, it would be interpreted as (not not) true, which is invalid. 77 "not not true", 78 // String functions 79 "contains(CompanyName,'freds')", 80 "endswith(CompanyName,'Futterkiste')", 81 "startswith(CompanyName,'Alfr')", 82 "length(CompanyName) eq 19", 83 "indexof(CompanyName,'lfreds') eq 1", 84 "substring(CompanyName,1) eq 'lfreds Futterkiste'", // substring() with 2 arguments. 85 "'lfreds Futterkiste' eq substring(CompanyName,1)", // Same as above, but order of operands is reversed. 86 "substring(CompanyName,1,2) eq 'lf'", // substring() with 3 arguments. 87 "'lf' eq substring(CompanyName,1,2) ", // Same as above, but order of operands is reversed. 88 "substringof('Alfreds', CompanyName) eq true", 89 "tolower(CompanyName) eq 'alfreds futterkiste'", 90 "toupper(CompanyName) eq 'ALFREDS FUTTERKISTE'", 91 "trim(CompanyName) eq 'Alfreds Futterkiste'", 92 "concat(concat(City,', '), Country) eq 'Berlin, Germany'", 93 // GUID 94 "GuidValue eq 01234567-89ab-cdef-0123-456789abcdef", // According to ODATA ABNF notation, GUID values do not have quotes. 95 // Date and Time functions 96 "StartDate eq 2012-12-03", 97 "DateTimeOffsetValue eq 2012-12-03T07:16:23Z", 98 // duration = [ "duration" ] SQUOTE durationValue SQUOTE 99 "DurationValue eq duration'P12DT23H59M59.999999999999S'", // See ODATA ABNF notation 100 "TimeOfDayValue eq 07:59:59.999", 101 "year(BirthDate) eq 0", 102 "month(BirthDate) eq 12", 103 "day(StartTime) eq 8", 104 "hour(StartTime) eq 1", 105 "hour (StartTime) eq 12", // function followed by space characters 106 "hour ( StartTime ) eq 15", // function followed by space characters 107 "minute(StartTime) eq 0", 108 "totaloffsetminutes(StartTime) eq 0", 109 "second(StartTime) eq 0", 110 "fractionalseconds(StartTime) lt 0.123456", // The fractionalseconds function returns the fractional seconds component of the 111 // DateTimeOffset or TimeOfDay parameter value as a non-negative decimal value less than 1. 112 "date(StartTime) ne date(EndTime)", 113 "totaloffsetminutes(StartTime) eq 60", 114 "StartTime eq mindatetime()", 115 "totalseconds(EndTime sub StartTime) lt duration'PT23H59M'", // The totalseconds function returns the duration of the value in total seconds, including fractional seconds. 116 "EndTime eq maxdatetime()", 117 "time(StartTime) le StartOfDay", 118 "time('2015-10-14T23:30:00.104+02:00') lt now()", 119 "time(2015-10-14T23:30:00.104+02:00) lt now()", 120 // Math functions 121 "round(Freight) eq 32", 122 "floor(Freight) eq 32", 123 "ceiling(Freight) eq 33", 124 "Rating mod 5 eq 0", 125 "Price div 2 eq 3", 126 // Functions 127 "contains(Name,'Ted')", 128 "startswith(Name,'Ted')", 129 "endswith(Name,'Lasso')", 130 "isof(ShipCountry,Edm.String)", 131 "isof(NorthwindModel.BigOrder)", 132 // Parameter aliases 133 // See http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part1-protocol/odata-v4.0-errata03-os-part1-protocol-complete.html#_Toc453752288 134 "Region eq @p1", // Aliases start with @ 135 // Logical operators 136 "'Milk' eq 'Milk'", // Compare two literals 137 "'Water' ne 'Milk'", // Compare two literals 138 "Name eq 'Milk'", 139 "Name EQ 'Milk'", // operators are case insensitive in ODATA 4.0.1 140 "Name ne 'Milk'", 141 "Name NE 'Milk'", 142 "Name gt 'Milk'", 143 "Name ge 'Milk'", 144 "Name lt 'Milk'", 145 "Name le 'Milk'", 146 "Name eq Name", // parameter equals to itself 147 "Name eq 'Milk' and Price lt 2.55", 148 "not endswith(Name,'ilk')", 149 "Name eq 'Milk' or Price lt 2.55", 150 "City eq 'Dallas' or City eq 'Houston'", 151 // Nested properties 152 "Product/Name eq 'Milk'", 153 "Region/Product/Name eq 'Milk'", 154 "Country/Region/Product/Name eq 'Milk'", 155 //"style has Sales.Pattern'Yellow'", // TODO 156 // Arithmetic operators 157 "Price add 2.45 eq 5.00", 158 "2.46 add Price eq 5.00", 159 "Price add (2.47) eq 5.00", 160 "(Price add (2.48)) eq 5.00", 161 "Price ADD 2.49 eq 5.00", // 4.01 Services MUST support case-insensitive operator names. 162 "Price sub 0.55 eq 2.00", 163 "Price SUB 0.56 EQ 2.00", // 4.01 Services MUST support case-insensitive operator names. 164 "Price mul 2.0 eq 5.10", 165 "Price div 2.55 eq 1", 166 "Rating div 2 eq 2", 167 "Rating mod 5 eq 0", 168 // Grouping 169 "(4 add 5) mod (4 sub 1) eq 0", 170 "not (City eq 'Dallas') or Name in ('a', 'b', 'c') and not (State eq 'California')", 171 // Nested functions 172 "length(trim(CompanyName)) eq length(CompanyName)", 173 "concat(concat(City, ', '), Country) eq 'Berlin, Germany'", 174 // Various parenthesis combinations 175 "City eq 'Dallas'", 176 "City eq ('Dallas')", 177 "'Dallas' eq City", 178 "not (City eq 'Dallas')", 179 "City in ('Dallas')", 180 "(City in ('Dallas'))", 181 "(City in ('Dallas', 'Houston'))", 182 "not (City in ('Dallas'))", 183 "not (City in ('Dallas', 'Houston'))", 184 "not (((City eq 'Dallas')))", 185 "not(S1 eq 'foo')", 186 // Lambda operators 187 "Tags/any()", // The any operator without an argument returns true if the collection is not empty 188 "Tags/any(tag:tag eq 'London')", // 'Tags' is array of strings 189 "Tags/any(tag:tag eq 'London' or tag eq 'Berlin')", // 'Tags' is array of strings 190 "Tags/any(var:var/Key eq 'Site' and var/Value eq 'London')", // 'Tags' is array of {"Key": "abc", "Value": "def"} 191 "Tags/ANY(var:var/Key eq 'Site' AND var/Value eq 'London')", 192 "Tags/any(var:var/Key eq 'Site' and var/Value eq 'London') and not (City in ('Dallas'))", 193 "Tags/all(var:var/Key eq 'Site' and var/Value eq 'London')", 194 "Price/any(t:not (12345 eq t))", 195 // A long query. 196 "Tags/any(var:var/Key eq 'Site' and var/Value eq 'London') or " + 197 "Tags/any(var:var/Key eq 'Site' and var/Value eq 'Berlin') or " + 198 "Tags/any(var:var/Key eq 'Site' and var/Value eq 'Paris') or " + 199 "Tags/any(var:var/Key eq 'Site' and var/Value eq 'New York City') or " + 200 "Tags/any(var:var/Key eq 'Site' and var/Value eq 'San Francisco')", 201 } 202 ctx := context.Background() 203 p := NewExpressionParser() 204 p.ExpectBoolExpr = true 205 for _, input := range queries { 206 t.Logf("Testing expression %s", input) 207 q, err := p.ParseExpressionString(ctx, input) 208 if err != nil { 209 t.Errorf("Error parsing query '%s'. Error: %v", input, err) 210 } else { 211 if q.Tree == nil { 212 t.Errorf("Error parsing query '%s'. Tree is nil", input) 213 } else if q.Tree.Token == nil { 214 t.Errorf("Error parsing query '%s'. Root token is nil", input) 215 } else if q.Tree.Token.Type == ExpressionTokenLiteral { 216 t.Errorf("Error parsing query '%s'. Unexpected root token type: %+v", input, q.Tree.Token) 217 } 218 } 219 //printTree(q.Tree) 220 } 221 } 222 223 // The URLs below are not valid ODATA syntax, the parser should return an error. 224 func TestInvalidBooleanExpressionSyntax(t *testing.T) { 225 ctx := context.Background() 226 queries := []string{ 227 "(TRUE)", // Should be true lowercase 228 "(City)", // The literal City is not boolean 229 "12345", // Number 12345 is not a boolean expression 230 "0", // Number 0 is not a boolean expression 231 "'123'", // String '123' is not a boolean expression 232 "TRUE", // Should be 'true' lowercase 233 "FALSE", // Should be 'false' lowercase 234 "yes", // yes is not a boolean expression, though it's a literal value 235 "no", // yes is not a boolean expression, though it's a literal value 236 "add 2 3", // Missing operands 237 "City", // Just a single literal 238 "Tags/any(var:var/Key eq 'Site') orTags/any(var:var/Key eq 'Site')", 239 "contains(Name, 'a', 'b', 'c', 'd')", // Too many function arguments 240 "cast(ShipCountry,Edm.String)", 241 // Geo functions 242 "geo.distance(CurrentPosition,TargetPosition)", 243 "geo.length(DirectRoute)", 244 "geo.intersects(Position,TargetArea)", 245 "GEO.INTERSECTS(Position,TargetArea)", // functions are case insensitive in ODATA 4.0.1 246 "now()", 247 "tolower(Name)", 248 "concat(First,Last)", 249 "case(false:0,true:1)", 250 } 251 p := NewExpressionParser() 252 p.ExpectBoolExpr = true 253 for _, input := range queries { 254 q, err := p.ParseExpressionString(ctx, input) 255 if err == nil { 256 // The parser has incorrectly determined the syntax is valid. 257 t.Errorf("The expression '%s' is not valid ODATA syntax. The ODATA parser should return an error. Tree:\n%v", input, q.Tree) 258 } 259 } 260 } 261 262 func TestExpressionWithLenientFlags(t *testing.T) { 263 testCases := []struct { 264 expression string 265 valid bool // true if parsing expression should be successful. 266 cfg OdataComplianceConfig 267 setCtx bool 268 tree []expectedParseNode // The expected tree. 269 }{ 270 { 271 expression: "(a, b, )", 272 valid: false, 273 setCtx: false, 274 }, 275 { 276 expression: "(a, b, )", 277 valid: false, 278 setCtx: true, 279 }, 280 { 281 expression: "(a, b, )", 282 valid: false, 283 setCtx: true, 284 cfg: ComplianceStrict, 285 }, 286 { 287 expression: "(a, b, )", 288 valid: true, // Normally this would not be valid, but the ComplianceIgnoreInvalidComma flag is set. 289 setCtx: true, 290 cfg: ComplianceIgnoreInvalidComma, 291 }, 292 { 293 expression: "City in ('Dallas', 'Houston', )", 294 valid: true, 295 setCtx: true, 296 cfg: ComplianceIgnoreInvalidComma, 297 tree: []expectedParseNode{ 298 {Value: "in", Depth: 0, Type: ExpressionTokenLogical}, 299 {Value: "City", Depth: 1, Type: ExpressionTokenLiteral}, 300 {Value: TokenListExpr, Depth: 1, Type: TokenTypeListExpr}, 301 {Value: "'Dallas'", Depth: 2, Type: ExpressionTokenString}, 302 {Value: "'Houston'", Depth: 2, Type: ExpressionTokenString}, 303 }, 304 }, 305 { 306 expression: "(a, , b)", // This is not a list. 307 valid: false, 308 }, 309 { 310 expression: "(, a, b)", // This is not a list. 311 valid: false, 312 }, 313 { 314 expression: "(,)", // A comma by itself is not an expression 315 valid: false, 316 }, 317 { 318 expression: "(,)", // A comma by itself is not an expression 319 valid: false, 320 setCtx: true, 321 cfg: ComplianceIgnoreInvalidComma, 322 }, 323 } 324 325 p := NewExpressionParser() 326 p.ExpectBoolExpr = false 327 for _, testCase := range testCases { 328 t.Logf("testing: %s", testCase.expression) 329 ctx := context.Background() 330 if testCase.setCtx { 331 ctx = WithOdataComplianceConfig(ctx, testCase.cfg) 332 } 333 q, err := p.ParseExpressionString(ctx, testCase.expression) 334 if testCase.valid && err != nil { 335 // The parser has incorrectly determined the syntax is invalid. 336 t.Errorf("The expression '%s' is valid ODATA syntax. Cfg: %v The ODATA parser should not have returned an error", 337 testCase.expression, testCase.cfg) 338 } else if !testCase.valid && err == nil { 339 // The parser has incorrectly determined the syntax is valid. 340 t.Errorf("The expression '%s' is not valid ODATA syntax. The ODATA parser should return an error. Tree:\n%v", 341 testCase.expression, q.Tree) 342 } else if testCase.valid && testCase.tree != nil { 343 pos := 0 344 err = CompareTree(q.Tree, testCase.tree, &pos, 0) 345 if err != nil { 346 t.Errorf("Tree representation does not match expected value. error: %v. Tree:\n%v", err, q.Tree) 347 } 348 } 349 } 350 } 351 352 func TestInvalidExpressionSyntax(t *testing.T) { 353 queries := []string{ 354 "()", // It's not a boolean expression 355 "(", 356 "((((", 357 ")", 358 "", // Empty string. 359 "eq", // Just a single logical operator 360 "and", // Just a single logical operator 361 "add", // Just a single arithmetic operator 362 "add ", // Just a single arithmetic operator 363 "add 2", // Missing operands 364 "City City City City", // Sequence of literals 365 "City eq", // Missing operand 366 "City eq (", // Wrong operand 367 "City eq )", // Wrong operand 368 "City equals 'Dallas'", // Unknown operator that starts with the same letters as a known operator 369 "City near 'Dallas'", // Unknown operator that starts with the same letters as a known operator 370 "City isNot 'Dallas'", // Unknown operator 371 "not [City eq 'Dallas']", // Wrong delimiter 372 "not (City eq )", // Missing operand 373 "not ((City eq 'Dallas'", // Missing closing parenthesis 374 "not (City eq 'Dallas'", // Missing closing parenthesis 375 "not (City eq 'Dallas'))", // Extraneous closing parenthesis 376 "not City eq 'Dallas')", // Missing open parenthesis 377 "City eq 'Dallas' orCity eq 'Houston'", // missing space between or and City 378 "not (City eq 'Dallas') and Name eq 'Houston')", 379 "Tags/all()", // The all operator cannot be used without an argument expression. 380 "LastName contains 'Smith'", // Previously the godata library was not returning an error. 381 "contains", // Function with missing parenthesis and arguments 382 "contains()", // Function with missing arguments 383 "contains LastName, 'Smith'", // Missing parenthesis 384 "contains(LastName)", // Insufficent number of function arguments 385 "contains(LastName, 'Smith'))", // Extraneous closing parenthesis 386 "contains(LastName, 'Smith'", // Missing closing parenthesis 387 "contains LastName, 'Smith')", // Missing open parenthesis 388 "City eq 'Dallas' 'Houston'", // extraneous string value 389 "(numCore neq 12)", // Invalid operator. It should be 'ne' 390 "numCore neq 12", // Invalid operator. It should be 'ne' 391 "(a b c d e)", // This is not a list. 392 "(a, b, )", // This is not a list. 393 "(a, , b)", // This is not a list. 394 "(, a, b)", // This is not a list. 395 "(a, not b c)", // Missing comma between (not b) and (c) 396 ",", // A comma by itself is not an expression 397 ",,,", // A comma by itself is not an expression 398 "(,)", // A comma by itself is not an expression 399 "contains(LastName, 'Smith'),", // Extra comma after the function call 400 "contains(LastName, 'Smith',)", // Extra comma after the last argument 401 "contains(,LastName, 'Smith')", // Extra comma before the first argument 402 "eq eq eq", // Invalid sequence of operators 403 "not not", // Invalid sequence of operators 404 "true true", // Invalid sequence of booleans 405 "1 2 3", // Invalid sequence of numbers 406 "1.4 2.34 3.1415", // Invalid sequence of numbers 407 "a b c", // Invalid sequence of literals. 408 "'a' 'b' 'c'", // Invalid sequence of strings. 409 } 410 ctx := context.Background() 411 p := NewExpressionParser() 412 p.ExpectBoolExpr = false 413 for _, input := range queries { 414 t.Logf("testing: %s", input) 415 q, err := p.ParseExpressionString(ctx, input) 416 if err == nil { 417 // The parser has incorrectly determined the syntax is valid. 418 t.Errorf("The expression '%s' is not valid ODATA syntax. The ODATA parser should return an error. Tree:\n%v", input, q.Tree) 419 } 420 } 421 } 422 423 func BenchmarkExpressionTokenizer(b *testing.B) { 424 ctx := context.Background() 425 t := NewExpressionTokenizer() 426 for i := 0; i < b.N; i++ { 427 input := "Name eq 'Milk' and Price lt 2.55" 428 if _, err := t.Tokenize(ctx, input); err != nil { 429 b.Fatalf("Failed to tokenize expression: %v", err) 430 } 431 } 432 } 433 434 func tokenArrayToString(list []*Token) string { 435 var sb []string 436 for _, t := range list { 437 sb = append(sb, fmt.Sprintf("%s[%d]", t.Value, t.Type)) 438 } 439 return strings.Join(sb, ", ") 440 } 441 442 // Check if two slices of tokens are the same. 443 func CompareTokens(expected, actual []*Token) (bool, error) { 444 if len(expected) != len(actual) { 445 return false, fmt.Errorf("Infix tokens unexpected lengths. Expected %d, Got len=%d. Tokens=%v", 446 len(expected), len(actual), tokenArrayToString(actual)) 447 } 448 for i := range expected { 449 if expected[i].Type != actual[i].Type { 450 return false, fmt.Errorf("Infix token types at index %d. Expected %v, Got %v. Value: %v", 451 i, expected[i].Type, actual[i].Type, expected[i].Value) 452 } 453 if expected[i].Value != actual[i].Value { 454 return false, fmt.Errorf("Infix token values at index %d. Expected %v, Got %v", 455 i, expected[i].Value, actual[i].Value) 456 } 457 } 458 return true, nil 459 } 460 461 func CompareQueue(expect []*Token, b *tokenQueue) error { 462 if b == nil { 463 return fmt.Errorf("Got nil token queue") 464 } 465 bl := func() int { 466 if b.Empty() { 467 return 0 468 } 469 l := 1 470 for node := b.Head; node != b.Tail; node = node.Next { 471 l++ 472 } 473 return l 474 }() 475 if len(expect) != bl { 476 return fmt.Errorf("Postfix queue unexpected length. Got len=%d, expected %d. queue=%v", 477 bl, len(expect), b) 478 } 479 node := b.Head 480 for i := range expect { 481 if expect[i].Type != node.Token.Type { 482 return fmt.Errorf("Postfix token types at index %d. Got: %v, expected: %v. Expected value: %v", 483 i, node.Token.Type, expect[i].Type, expect[i].Value) 484 } 485 if expect[i].Value != node.Token.Value { 486 return fmt.Errorf("Postfix token values at index %d. Got: %v, expected: %v", 487 i, node.Token.Value, expect[i].Value) 488 } 489 node = node.Next 490 } 491 return nil 492 } 493 494 func printTokens(tokens []*Token) { 495 s := make([]string, len(tokens)) 496 for i := range tokens { 497 s[i] = tokens[i].Value 498 } 499 fmt.Printf("TOKENS: %s\n", strings.Join(s, " ")) 500 } 501 502 // CompareTree compares a tree representing a ODATA filter with the expected results. 503 // The expected values are a slice of nodes in breadth-first traversal. 504 func CompareTree(node *ParseNode, expect []expectedParseNode, pos *int, level int) error { 505 if *pos >= len(expect) { 506 return fmt.Errorf("Unexpected token at pos %d. Got %s, expected no value", 507 *pos, node.Token.Value) 508 } 509 if node == nil { 510 return fmt.Errorf("Node should not be nil") 511 } 512 if node.Token.Value != 513 expect[*pos].Value { 514 return fmt.Errorf("Unexpected token at pos %d. Got %s -> %d, expected: %s -> %d", 515 *pos, node.Token.Value, level, expect[*pos].Value, expect[*pos].Depth) 516 } 517 if node.Token.Type != expect[*pos].Type { 518 return fmt.Errorf("Unexpected token type at pos %d. Got %v -> %d, expected: %v -> %d", 519 *pos, node.Token.Type, level, expect[*pos].Type, expect[*pos].Depth) 520 } 521 if level != expect[*pos].Depth { 522 return fmt.Errorf("Unexpected level at pos %d. Got %s -> %d, expected: %s -> %d", 523 *pos, node.Token.Value, level, expect[*pos].Value, expect[*pos].Depth) 524 } 525 for _, v := range node.Children { 526 *pos++ 527 if err := CompareTree(v, expect, pos, level+1); err != nil { 528 return err 529 } 530 } 531 if level == 0 && *pos+1 != len(expect) { 532 return fmt.Errorf("Expected number of tokens: %d, got %d", len(expect), *pos+1) 533 } 534 return nil 535 } 536 537 func TestExpressions(t *testing.T) { 538 ctx := context.Background() 539 p := NewExpressionParser() 540 for _, testCase := range testCases { 541 t.Logf("Expression: %s", testCase.expression) 542 tokens, err := GlobalExpressionTokenizer.Tokenize(ctx, testCase.expression) 543 if err != nil { 544 t.Errorf("Failed to tokenize expression '%s'. Error: %v", testCase.expression, err) 545 continue 546 } 547 if testCase.infixTokens != nil { 548 if result, err := CompareTokens(testCase.infixTokens, tokens); !result { 549 t.Errorf("Unexpected tokens: %v", err) 550 continue 551 } 552 } 553 output, err := p.InfixToPostfix(ctx, tokens) 554 if err != nil { 555 t.Errorf("Failed to convert expression to postfix notation: %v", err) 556 continue 557 } 558 if testCase.postfixTokens != nil { 559 if err := CompareQueue(testCase.postfixTokens, output); err != nil { 560 t.Errorf("Unexpected postfix tokens: %v", err) 561 continue 562 } 563 } 564 tree, err := p.PostfixToTree(ctx, output) 565 if err != nil { 566 t.Errorf("Failed to parse expression '%s'. Error: %v", testCase.expression, err) 567 continue 568 } 569 pos := 0 570 err = CompareTree(tree, testCase.tree, &pos, 0) 571 if err != nil { 572 t.Errorf("Tree representation does not match expected value. error: %v. Tree:\n%v", err, tree) 573 } 574 } 575 576 } 577 func TestDuration(t *testing.T) { 578 testCases := []struct { 579 value string 580 valid bool 581 }{ 582 {value: "duration'P12DT23H59M59.999999999999S'", valid: true}, 583 // three years, six months, four days, twelve hours, thirty minutes, and five seconds 584 {value: "duration'P3Y6M4DT12H30M5S'", valid: true}, 585 // Date and time elements including their designator may be omitted if their value is zero, 586 // and lower-order elements may also be omitted for reduced precision. 587 {value: "duration'P23DT23H'", valid: true}, 588 {value: "duration'P4Y'", valid: true}, 589 // However, at least one element must be present, 590 // thus "P" is not a valid representation for a duration of 0 seconds. 591 {value: "duration'P'", valid: false}, 592 // "PT0S" or "P0D", however, are both valid and represent the same duration. 593 {value: "duration'PT0S'", valid: true}, 594 {value: "duration'P0D'", valid: true}, 595 // To resolve ambiguity, "P1M" is a one-month duration and "PT1M" is a one-minute duration 596 {value: "duration'P1M'", valid: true}, 597 {value: "duration'PT1M'", valid: true}, 598 // The standard does not prohibit date and time values in a duration representation 599 // from exceeding their "carry over points" except as noted below. 600 // Thus, "PT36H" could be used as well as "P1DT12H" for representing the same duration. 601 {value: "duration'PT36H'", valid: true}, 602 {value: "duration'P1DT12H'", valid: true}, 603 {value: "duration'PT23H59M'", valid: true}, 604 {value: "duration'PT23H59'", valid: false}, // missing units 605 606 {value: "duration'H0D'", valid: false}, 607 {value: "foo", valid: false}, 608 609 // TODO: the duration values below should be valid 610 // The smallest value used may also have a decimal fraction,[35] as in "P0.5Y" to indicate half a year. 611 {value: "duration'P0.5Y'", valid: false}, // half a year 612 {value: "duration'P0.5M'", valid: false}, // half a month 613 // This decimal fraction may be specified with either a comma or a full stop, as in "P0,5Y" or "P0.5Y". 614 {value: "duration'P0,5Y'", valid: false}, 615 } 616 re, err := regexp.Compile(tokenDurationRe) 617 if err != nil { 618 t.Fatalf("Invalid regex: %v", err) 619 } 620 for _, testCase := range testCases { 621 m := re.MatchString(testCase.value) 622 if m != testCase.valid { 623 t.Errorf("Value: %s. Expected regex match: %v, got %v", 624 testCase.value, testCase.valid, m) 625 } 626 } 627 }