|
<?php |
|
|
|
/** |
|
* Salesportal. |
|
* |
|
* @since 1.0.0 |
|
* @package Salesportal |
|
* @copyright 2021 Lacey Tech Solutions |
|
* @link https://lacey-tech.com |
|
*/ |
|
|
|
namespace Salesportal\Tests\Query; |
|
|
|
use DateTime; |
|
use DateTimeZone; |
|
use Generator; |
|
use PHPUnit\Framework\TestCase; |
|
use Salesportal\Query\Parser; |
|
use Salesportal\Query\Lexer; |
|
use Salesportal\Query\Token; |
|
use Salesportal\Query\ParseException; |
|
use Salesportal\Query\AST\Node; |
|
use Salesportal\Query\AST\BinaryTree; |
|
use Salesportal\Query\AST\Comparison; |
|
use Salesportal\Query\AST\Expression; |
|
use Salesportal\Query\AST\Identifier; |
|
use Salesportal\Query\AST\Value; |
|
|
|
/** |
|
* Tests the Query parser. |
|
* This tests that the logic strings are correctly |
|
* parsed and assumed into the correct formats and node |
|
* structure. |
|
* |
|
* @since 1.0.0 |
|
* @author Dom Webber <dom.webber@hotmail.com> |
|
*/ |
|
final class ParserTest extends TestCase |
|
{ |
|
/** |
|
* The Lexer instance to use with the Parser. |
|
* |
|
* @since 1.0.0 |
|
* |
|
* @var Lexer |
|
*/ |
|
private $lexer; |
|
|
|
/** |
|
* Setup. |
|
* |
|
* @since 1.0.0 |
|
* |
|
* @return void |
|
*/ |
|
protected function setUp(): void |
|
{ |
|
$container = require __DIR__ . "/../../app/bootstrap.php"; |
|
$this->lexer = $container->get(Lexer::class); |
|
} |
|
|
|
/** |
|
* Data provider for valid parsing tests. |
|
* |
|
* @since 1.0.0 |
|
* |
|
* @return Generator |
|
*/ |
|
public function validParseProvider(): Generator |
|
{ |
|
yield "Equals comparison with an integer" => [ |
|
"field = 12", |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("field") |
|
) |
|
->setValue(Lexer::EXPR_EQUALS) |
|
->setRight( |
|
(new Value())->setValue(12) |
|
) |
|
]; |
|
|
|
yield "Equals comparison to null" => [ |
|
"author = null", |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("author") |
|
) |
|
->setValue(Lexer::EXPR_EQUALS) |
|
->setRight( |
|
(new Value())->setValue(null) |
|
) |
|
]; |
|
|
|
yield "Bracket-ed equals comparison to a single-quoted string" => [ |
|
"(name = 'Dom')", |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("name") |
|
) |
|
->setValue(Lexer::EXPR_EQUALS) |
|
->setRight( |
|
(new Value())->setValue("Dom") |
|
) |
|
]; |
|
|
|
yield "Not-equals comparison to float" => [ |
|
"height != 12.25", |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("height") |
|
) |
|
->setValue(Lexer::EXPR_NOT_EQUALS) |
|
->setRight( |
|
(new Value())->setValue(12.25) |
|
) |
|
]; |
|
|
|
yield "Less than comparison to DateTime" => [ |
|
"created < '2021-01-08T01:43:34+0000'", |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("created") |
|
) |
|
->setValue(Lexer::EXPR_LESS_THAN) |
|
->setRight( |
|
(new Value())->setValue( |
|
DateTime::createFromFormat( |
|
DateTime::ISO8601, |
|
"2021-01-08T01:43:34+0000", |
|
new DateTimeZone("UTC") |
|
) |
|
) |
|
) |
|
]; |
|
|
|
yield "Equals comparison to boolean true" => [ |
|
"enabled = true", |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("enabled") |
|
) |
|
->setValue(Lexer::EXPR_EQUALS) |
|
->setRight( |
|
(new Value())->setValue(true) |
|
) |
|
]; |
|
|
|
yield "Less than equals comparison to float" => [ |
|
"price <= 12.20", |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("price") |
|
) |
|
->setValue(Lexer::EXPR_LESS_THAN_EQUALS) |
|
->setRight( |
|
(new Value())->setValue(12.20) |
|
) |
|
]; |
|
|
|
yield "Greater than comparison to DateTime" => [ |
|
"modified > \"2021-01-08T01:43:34+0000\"", |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("modified") |
|
) |
|
->setValue(Lexer::EXPR_GREATER_THAN) |
|
->setRight( |
|
(new Value())->setValue( |
|
DateTime::createFromFormat( |
|
DateTime::ISO8601, |
|
"2021-01-08T01:43:34+0000", |
|
new DateTimeZone("UTC") |
|
) |
|
) |
|
) |
|
]; |
|
|
|
yield "Greater than equals comparison to scientific \"e\" notation" => [ |
|
"distance >= 6.022e23", |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("distance") |
|
) |
|
->setValue(Lexer::EXPR_GREATER_THAN_EQUALS) |
|
->setRight( |
|
(new Value())->setValue(6.022e23) |
|
) |
|
]; |
|
|
|
yield "Like comparison to single-quoted string" => [ |
|
"description like 'impressive'", |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("description") |
|
) |
|
->setValue(Lexer::EXPR_LIKE) |
|
->setRight( |
|
(new Value())->setValue("impressive") |
|
) |
|
]; |
|
|
|
yield "Complex query with a right-side subquery" => [ |
|
"username = 'domwebber' or ( email = 'dom.webber@hotmail.com' and telephone = '01234567890' )", |
|
(new Expression()) |
|
->setLeft( |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("username") |
|
) |
|
->setValue(Lexer::EXPR_EQUALS) |
|
->setRight( |
|
(new Value())->setValue("domwebber") |
|
) |
|
) |
|
->setValue(Lexer::EXPR_OR) |
|
->setRight( |
|
(new Expression()) |
|
->setLeft( |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("email") |
|
) |
|
->setValue(Lexer::EXPR_EQUALS) |
|
->setRight( |
|
(new Value())->setValue("dom.webber@hotmail.com") |
|
) |
|
) |
|
->setValue(Lexer::EXPR_AND) |
|
->setRight( |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("telephone") |
|
) |
|
->setValue(Lexer::EXPR_EQUALS) |
|
->setRight( |
|
(new Value())->setValue("01234567890") |
|
) |
|
) |
|
) |
|
]; |
|
|
|
yield "Complex query with subqueries on both sides" => [ |
|
"( name != 'Dom' or time != '2021-01-08T01:43:34+0000' ) or ( time = '2021-01-08T01:43:34+0000' and enabled = true )", |
|
(new Expression()) |
|
->setLeft( |
|
(new Expression()) |
|
->setLeft( |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("name") |
|
) |
|
->setValue(Lexer::EXPR_NOT_EQUALS) |
|
->setRight( |
|
(new Value())->setValue("Dom") |
|
) |
|
) |
|
->setValue(Lexer::EXPR_OR) |
|
->setRight( |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("time") |
|
) |
|
->setValue(Lexer::EXPR_NOT_EQUALS) |
|
->setRight( |
|
(new Value())->setValue( |
|
DateTime::createFromFormat( |
|
DateTime::ISO8601, |
|
"2021-01-08T01:43:34+0000", |
|
new DateTimeZone("UTC") |
|
) |
|
) |
|
) |
|
) |
|
) |
|
->setValue(Lexer::EXPR_OR) |
|
->setRight( |
|
(new Expression()) |
|
->setLeft( |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("time") |
|
) |
|
->setValue(Lexer::EXPR_EQUALS) |
|
->setRight( |
|
(new Value())->setValue( |
|
DateTime::createFromFormat( |
|
DateTime::ISO8601, |
|
"2021-01-08T01:43:34+0000", |
|
new DateTimeZone("UTC") |
|
) |
|
) |
|
) |
|
) |
|
->setValue(Lexer::EXPR_AND) |
|
->setRight( |
|
(new Comparison()) |
|
->setLeft( |
|
(new Identifier())->setValue("enabled") |
|
) |
|
->setValue(Lexer::EXPR_EQUALS) |
|
->setRight( |
|
(new Value())->setValue(true) |
|
) |
|
) |
|
) |
|
]; |
|
} |
|
|
|
/** |
|
* Test the Query Parser with valid logical expressions. |
|
* This tests that queries are represented after parsing as expected and that |
|
* the queries succeed in parsing. |
|
* |
|
* @since 1.0.0 |
|
* @dataProvider validParseProvider |
|
* |
|
* @param string $input |
|
* @param BinaryTree $expectation |
|
* @return void |
|
*/ |
|
public function testValidParse(string $input, BinaryTree $expectation): void |
|
{ |
|
//Create the parser instance |
|
$parser = new Parser($this->lexer); |
|
|
|
//Run the parser |
|
$output = $parser->parse($input); |
|
|
|
//Test the assertions |
|
$this->assertTrue($expectation == $output); |
|
} |
|
|
|
/** |
|
* Data provider for the invalid parsing tests. |
|
* |
|
* @since 1.0.0 |
|
* |
|
* @return Generator |
|
*/ |
|
public function invalidParseProvider(): Generator |
|
{ |
|
yield "Empty string" => [ |
|
"" |
|
]; |
|
|
|
yield "Missing right parenthesis" => [ |
|
"name = 'Dom' or (email = 'dom.webber@hotmail.com' and telephone = '01234567890' " |
|
]; |
|
|
|
yield "Misplaced right parenthesis" => [ |
|
"name = 'Dom' or email = 'dom.webber@hotmail.com' )" |
|
]; |
|
|
|
yield "Misformatted float in scientific notation" => [ |
|
"value = 6.022e" |
|
]; |
|
|
|
yield "Misformatted float" => [ |
|
"value = 12." |
|
]; |
|
|
|
yield "Missing ending quote in double-quoted string" => [ |
|
"name = \"Dom" |
|
]; |
|
|
|
yield "Missing start quote in double-quoted string" => [ |
|
"name = Dom\"" |
|
]; |
|
|
|
yield "Missing ending quote in single-quoted string" => [ |
|
"name = 'Dom" |
|
]; |
|
|
|
yield "Missing start quote in single-quoted string" => [ |
|
"name = Dom'" |
|
]; |
|
|
|
yield "Mispaced left parenthesis" => [ |
|
"( name = 'Dom'" |
|
]; |
|
|
|
yield "Incorrect order" => [ |
|
"'Dom' = name" |
|
]; |
|
|
|
yield "Logic operator instead of a comparison operator" => [ |
|
"name and 'Dom'" |
|
]; |
|
|
|
yield "Logic operator instead of a comparison operator and incorrect order" => [ |
|
"'Dom' and name" |
|
]; |
|
|
|
yield "Incorrect order in subquery" => [ |
|
"name = 'Dom' and ( name or 'Dom' )" |
|
]; |
|
|
|
yield "Misplaced comparison operator" => [ |
|
"name = < 12" |
|
]; |
|
|
|
yield "Missing value in comparison" => [ |
|
"name =" |
|
]; |
|
|
|
yield "Misplaced logic operator" => [ |
|
"name = 'Dom' and" |
|
]; |
|
|
|
yield "Invalid tokens and symbols" => [ |
|
"name = 'Dom' $@*&" |
|
]; |
|
|
|
yield "Value in expression in subquery" => [ |
|
"name = 'Dom' and ( null or null )" |
|
]; |
|
|
|
yield "Empty subquery parenthesises" => [ |
|
"name = 'Dom' and ()" |
|
]; |
|
|
|
yield "Misplaced logic operator and misplaced value" => [ |
|
"and 'Dom'" |
|
]; |
|
|
|
yield "Missing identifier and value in comparison" => [ |
|
"=" |
|
]; |
|
|
|
yield "Complex query with missing subquery end closing parenthesis" => [ |
|
"( name != 'Dom' or time != '2021-01-08T01:43:34+0000' ) or ( time = '2021-01-08T01:43:34+0000' and enabled = true " |
|
]; |
|
|
|
yield "Exhausting token limit" => [ |
|
str_repeat("enabled = false and ", 63) . "disabled = true" |
|
]; |
|
} |
|
|
|
/** |
|
* Test the Query Parser with invalid logical expressions. |
|
* This tests that incorrectly formatted and invalid query expressions fail. This |
|
* ensures that no precedence assumptions are made in the case that brackets are |
|
* missing and that expressions in the wrong order are not accepted into the system. |
|
* |
|
* @since 1.0.0 |
|
* @dataProvider invalidParseProvider |
|
* |
|
* @param string $input |
|
* @return void |
|
*/ |
|
public function testInvalidParse(string $input): void |
|
{ |
|
//Create the parser instance |
|
$parser = new Parser($this->lexer); |
|
|
|
$this->expectException(ParseException::class); |
|
|
|
//Run the parser |
|
$output = $parser->parse($input); |
|
} |
|
|
|
/** |
|
* Data provider for the valid comparison operator normalization tests. |
|
* |
|
* @since 1.0.0 |
|
* |
|
* @return Generator |
|
*/ |
|
public function validNormalizeComparisonOperatorProvider(): Generator |
|
{ |
|
yield "Equals comparison operator word" => [ |
|
Lexer::EXPR_EQUALS, |
|
new Token([ |
|
"value" => "eq", |
|
"type" => Lexer::T_EQUALS, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Equals comparison operator symbol" => [ |
|
Lexer::EXPR_EQUALS, |
|
new Token([ |
|
"value" => "=", |
|
"type" => Lexer::T_EQUALS, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Not equals comparison operator word" => [ |
|
Lexer::EXPR_NOT_EQUALS, |
|
new Token([ |
|
"value" => "ne", |
|
"type" => Lexer::T_NOT_EQUALS, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Not equals comparison operator symbol" => [ |
|
Lexer::EXPR_NOT_EQUALS, |
|
new Token([ |
|
"value" => "!=", |
|
"type" => Lexer::T_NOT_EQUALS, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Less than comparison operator word" => [ |
|
Lexer::EXPR_LESS_THAN, |
|
new Token([ |
|
"value" => "lt", |
|
"type" => Lexer::T_LESS_THAN, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Less than comparison operator symbol" => [ |
|
Lexer::EXPR_LESS_THAN, |
|
new Token([ |
|
"value" => "lt", |
|
"type" => Lexer::T_LESS_THAN, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Less than equals comparison operator word" => [ |
|
Lexer::EXPR_LESS_THAN_EQUALS, |
|
new Token([ |
|
"value" => "lte", |
|
"type" => Lexer::T_LESS_THAN_EQUALS, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Less than equals comparison operator symbol" => [ |
|
Lexer::EXPR_LESS_THAN_EQUALS, |
|
new Token([ |
|
"value" => "<=", |
|
"type" => Lexer::T_LESS_THAN_EQUALS, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Greater than comparison operator word" => [ |
|
Lexer::EXPR_GREATER_THAN, |
|
new Token([ |
|
"value" => "gt", |
|
"type" => Lexer::T_GREATER_THAN, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Greater than comparison operator symbol" => [ |
|
Lexer::EXPR_GREATER_THAN, |
|
new Token([ |
|
"value" => ">", |
|
"type" => Lexer::T_GREATER_THAN, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Greater than equals comparison operator word" => [ |
|
Lexer::EXPR_GREATER_THAN_EQUALS, |
|
new Token([ |
|
"value" => "gte", |
|
"type" => Lexer::T_GREATER_THAN_EQUALS, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Greater than equals comparison operator symbol" => [ |
|
Lexer::EXPR_GREATER_THAN_EQUALS, |
|
new Token([ |
|
"value" => ">=", |
|
"type" => Lexer::T_GREATER_THAN_EQUALS, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Like comparison operator (only exists as word)" => [ |
|
Lexer::EXPR_LIKE, |
|
new Token([ |
|
"value" => "contains", |
|
"type" => Lexer::T_LIKE, |
|
"position" => 0 |
|
]) |
|
]; |
|
} |
|
|
|
/** |
|
* Test the comparison operator normalization with valid values. |
|
* |
|
* @since 1.0.0 |
|
* @dataProvider validNormalizeComparisonOperatorProvider |
|
* |
|
* @param Token $token |
|
* @param string $expectation |
|
* @return void |
|
*/ |
|
public function testValidNormalizeComparisonOperator(string $expectation, Token $token): void |
|
{ |
|
//Extend the parser |
|
$parser = new class ($this->lexer) extends Parser |
|
{ |
|
/** |
|
* Provide testing access to the Comparison Operator normalization method. |
|
* |
|
* @since 1.0.0 |
|
* |
|
* @param string $value |
|
* @return string |
|
*/ |
|
public function testNormalizeComparisonOperator(Token $token): string |
|
{ |
|
return $this->normalizeComparisonOperator($token); |
|
} |
|
}; |
|
|
|
//Run the test method |
|
$output = $parser->testNormalizeComparisonOperator($token); |
|
$this->assertEquals($expectation, $output); |
|
} |
|
|
|
/** |
|
* Data provider for the invalid comparison operator normalization tests. |
|
* |
|
* @since 1.0.0 |
|
* |
|
* @return Generator |
|
*/ |
|
public function invalidNormalizeComparisonOperatorProvider(): Generator |
|
{ |
|
yield "Non-matching token type" => [ |
|
new Token([ |
|
"value" => "invalid", |
|
"type" => -1, |
|
"position" => 0 |
|
]) |
|
]; |
|
} |
|
|
|
/** |
|
* Test the comparison operator normalization with invalid values. |
|
* |
|
* @since 1.0.0 |
|
* @dataProvider invalidNormalizeComparisonOperatorProvider |
|
* |
|
* @param Token $token |
|
* @return void |
|
*/ |
|
public function testInvalidNormalizeComparisonOperator(Token $token): void |
|
{ |
|
//Extend the parser |
|
$parser = new class ($this->lexer) extends Parser |
|
{ |
|
/** |
|
* Provide testing access to the Comparison Operator normalization method. |
|
* |
|
* @since 1.0.0 |
|
* |
|
* @param string $value |
|
* @return string |
|
*/ |
|
public function testNormalizeComparisonOperator(Token $token): string |
|
{ |
|
return $this->normalizeComparisonOperator($token); |
|
} |
|
}; |
|
|
|
$this->expectException(ParseException::class); |
|
|
|
//Test with an invalid token |
|
$parser->testNormalizeComparisonOperator($token); |
|
} |
|
|
|
/** |
|
* Data provider for the valid logic operator normalization. |
|
* |
|
* @since 1.0.0 |
|
* |
|
* @return Generator |
|
*/ |
|
public function validNormalizeLogicOperatorProvider(): Generator |
|
{ |
|
yield "And operator word" => [ |
|
Lexer::EXPR_AND, |
|
new Token([ |
|
"value" => "and", |
|
"type" => Lexer::T_AND, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "And operator symbol" => [ |
|
Lexer::EXPR_AND, |
|
new Token([ |
|
"value" => "&&", |
|
"type" => Lexer::T_AND, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Or operator word" => [ |
|
Lexer::EXPR_OR, |
|
new Token([ |
|
"value" => "or", |
|
"type" => Lexer::T_OR, |
|
"position" => 0 |
|
]) |
|
]; |
|
|
|
yield "Or operator symbol" => [ |
|
Lexer::EXPR_OR, |
|
new Token([ |
|
"value" => "||", |
|
"type" => Lexer::T_OR, |
|
"position" => 0 |
|
]) |
|
]; |
|
} |
|
|
|
/** |
|
* Test the logic operator normalization with valid values. |
|
* |
|
* @since 1.0.0 |
|
* @dataProvider validNormalizeLogicOperatorProvider |
|
* |
|
* @return void |
|
*/ |
|
public function testValidNormalizeLogicOperator(string $expectation, Token $input_token): void |
|
{ |
|
//Extend the parser |
|
$parser = new class ($this->lexer) extends Parser |
|
{ |
|
/** |
|
* Provide testing access to the Logic Operator normalization method. |
|
* |
|
* @since 1.0.0 |
|
* |
|
* @param string $value |
|
* @return string |
|
*/ |
|
public function testNormalizeLogicOperator(Token $token): string |
|
{ |
|
return $this->normalizeLogicOperator($token); |
|
} |
|
}; |
|
|
|
//Run the test method |
|
$output = $parser->testNormalizeLogicOperator($input_token); |
|
$this->assertEquals($expectation, $output); |
|
} |
|
|
|
/** |
|
* Data provider for the invalid logic operator normalization tests. |
|
* |
|
* @since 1.0.0 |
|
* |
|
* @return Generator |
|
*/ |
|
public function invalidNormalizeLogicOperatorProvider(): Generator |
|
{ |
|
yield "Non-matching token type" => [ |
|
new Token([ |
|
"value" => "invalid", |
|
"type" => -1, |
|
"position" => 0 |
|
]) |
|
]; |
|
} |
|
|
|
/** |
|
* Test the logic operator normalization with invalid values. |
|
* |
|
* @since 1.0.0 |
|
* @dataProvider invalidNormalizeLogicOperatorProvider |
|
* |
|
* @param Token $token |
|
* @return void |
|
*/ |
|
public function testInvalidNormalizeLogicOperator(Token $input_token): void |
|
{ |
|
//Extend the parser |
|
$parser = new class ($this->lexer) extends Parser |
|
{ |
|
/** |
|
* Provide testing access to the Logic Operator normalization method. |
|
* |
|
* @since 1.0.0 |
|
* |
|
* @param string $value |
|
* @return string |
|
*/ |
|
public function testNormalizeLogicOperator(Token $token): string |
|
{ |
|
return $this->normalizeLogicOperator($token); |
|
} |
|
}; |
|
|
|
$this->expectException(ParseException::class); |
|
|
|
//Test with an invalid token |
|
$parser->testNormalizeLogicOperator($input_token); |
|
} |
|
} |