347 lines
13 KiB
PHP
347 lines
13 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace Tests\Unit\Rules;
|
||
|
|
|
||
|
|
use App\Services\RuleExpressionService;
|
||
|
|
use CodeIgniter\Test\CIUnitTestCase;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Tests for Rule DSL syntax - semicolon syntax, multi-actions, operators
|
||
|
|
*/
|
||
|
|
class RuleExpressionSyntaxTest extends CIUnitTestCase
|
||
|
|
{
|
||
|
|
protected RuleExpressionService $service;
|
||
|
|
|
||
|
|
public function setUp(): void
|
||
|
|
{
|
||
|
|
parent::setUp();
|
||
|
|
$this->service = new RuleExpressionService();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// SYNTAX TESTS
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
public function testSemicolonSyntaxBasic()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(sex("M"); result_set(0.5); result_set(0.6))');
|
||
|
|
|
||
|
|
$this->assertArrayHasKey('conditionExpr', $result);
|
||
|
|
$this->assertArrayHasKey('valueExpr', $result);
|
||
|
|
$this->assertArrayHasKey('then', $result);
|
||
|
|
$this->assertArrayHasKey('else', $result);
|
||
|
|
|
||
|
|
$this->assertEquals('patient["Sex"] == "M"', $result['conditionExpr']);
|
||
|
|
$this->assertEquals('0.5', $result['then'][0]['valueExpr']);
|
||
|
|
$this->assertEquals('0.6', $result['else'][0]['valueExpr']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testTernarySyntaxFallback()
|
||
|
|
{
|
||
|
|
// Legacy ternary syntax should still work (fallback)
|
||
|
|
$result = $this->service->compile('if(sex("M") ? result_set(0.5) : result_set(0.6))');
|
||
|
|
|
||
|
|
$this->assertArrayHasKey('conditionExpr', $result);
|
||
|
|
$this->assertEquals('patient["Sex"] == "M"', $result['conditionExpr']);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// MULTI-ACTION TESTS
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
public function testMultiActionWithColon()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(sex("M"); result_set(0.5):test_insert("HBA1C"); nothing)');
|
||
|
|
|
||
|
|
$this->assertCount(2, $result['then']);
|
||
|
|
$this->assertEquals('RESULT_SET', $result['then'][0]['type']);
|
||
|
|
$this->assertEquals(0.5, $result['then'][0]['value']);
|
||
|
|
$this->assertEquals('TEST_INSERT', $result['then'][1]['type']);
|
||
|
|
$this->assertEquals('HBA1C', $result['then'][1]['testCode']);
|
||
|
|
$this->assertEquals('NO_OP', $result['else'][0]['type']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testThreeActions()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(priority("S"); result_set("URGENT"):test_insert("STAT_TEST"):comment_insert("Stat order"); nothing)');
|
||
|
|
|
||
|
|
$this->assertCount(3, $result['then']);
|
||
|
|
$this->assertEquals('RESULT_SET', $result['then'][0]['type']);
|
||
|
|
$this->assertEquals('URGENT', $result['then'][0]['value']);
|
||
|
|
$this->assertEquals('TEST_INSERT', $result['then'][1]['type']);
|
||
|
|
$this->assertEquals('STAT_TEST', $result['then'][1]['testCode']);
|
||
|
|
$this->assertEquals('COMMENT_INSERT', $result['then'][2]['type']);
|
||
|
|
$this->assertEquals('Stat order', $result['then'][2]['comment']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testMultiActionInElse()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(sex("M"); result_set(1.0); result_set(0.8):comment_insert("Default"))');
|
||
|
|
|
||
|
|
$this->assertCount(1, $result['then']);
|
||
|
|
$this->assertCount(2, $result['else']);
|
||
|
|
$this->assertEquals('RESULT_SET', $result['else'][0]['type']);
|
||
|
|
$this->assertEquals(0.8, $result['else'][0]['value']);
|
||
|
|
$this->assertEquals('COMMENT_INSERT', $result['else'][1]['type']);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// LOGICAL OPERATOR TESTS
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
public function testAndOperator()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(sex("M") && age > 40; result_set(1.2); result_set(1.0))');
|
||
|
|
|
||
|
|
$this->assertStringContainsString('and', strtolower($result['conditionExpr']));
|
||
|
|
$this->assertStringContainsString('patient["Sex"] == "M"', $result['conditionExpr']);
|
||
|
|
$this->assertStringContainsString('age > 40', $result['conditionExpr']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testOrOperator()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(sex("M") || age > 65; result_set(1.0); result_set(0.8))');
|
||
|
|
|
||
|
|
$this->assertStringContainsString('or', strtolower($result['conditionExpr']));
|
||
|
|
$this->assertStringContainsString('patient["Sex"] == "M"', $result['conditionExpr']);
|
||
|
|
$this->assertStringContainsString('age > 65', $result['conditionExpr']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testCombinedAndOr()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if((sex("M") && age > 40) || (sex("F") && age > 50); result_set(1.5); result_set(1.0))');
|
||
|
|
|
||
|
|
$this->assertStringContainsString('or', strtolower($result['conditionExpr']));
|
||
|
|
$this->assertStringContainsString('and', strtolower($result['conditionExpr']));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testComplexNestedCondition()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(sex("M") && (age > 40 || priority("S")); result_set(1.2); nothing)');
|
||
|
|
|
||
|
|
$this->assertStringContainsString('patient["Sex"] == "M"', $result['conditionExpr']);
|
||
|
|
$this->assertStringContainsString('or', strtolower($result['conditionExpr']));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// ACTION TESTS
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
public function testNothingAction()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(sex("M"); result_set(0.5); nothing)');
|
||
|
|
|
||
|
|
$this->assertEquals('NO_OP', $result['else'][0]['type']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testNothingActionInThen()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(sex("M"); nothing; result_set(0.6))');
|
||
|
|
|
||
|
|
$this->assertEquals('NO_OP', $result['then'][0]['type']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testInsertAction()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(requested("GLU"); test_insert("HBA1C"); nothing)');
|
||
|
|
|
||
|
|
$this->assertEquals('TEST_INSERT', $result['then'][0]['type']);
|
||
|
|
$this->assertEquals('HBA1C', $result['then'][0]['testCode']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testAddCommentAction()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(sex("M"); comment_insert("Male patient"); nothing)');
|
||
|
|
|
||
|
|
$this->assertEquals('COMMENT_INSERT', $result['then'][0]['type']);
|
||
|
|
$this->assertEquals('Male patient', $result['then'][0]['comment']);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// CONDITION TESTS
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
public function testSexCondition()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(sex("F"); result_set(0.7); result_set(1.0))');
|
||
|
|
|
||
|
|
$this->assertEquals('patient["Sex"] == "F"', $result['conditionExpr']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testPriorityCondition()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(priority("S"); result_set("URGENT"); result_set("NORMAL"))');
|
||
|
|
|
||
|
|
$this->assertEquals('order["Priority"] == "S"', $result['conditionExpr']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testPriorityUrgent()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(priority("U"); result_set("CRITICAL"); nothing)');
|
||
|
|
|
||
|
|
$this->assertEquals('order["Priority"] == "U"', $result['conditionExpr']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testAgeCondition()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(age > 18; result_set(1.0); result_set(0.5))');
|
||
|
|
|
||
|
|
$this->assertEquals('age > 18', $result['conditionExpr']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testAgeGreaterThanEqual()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(age >= 18; result_set(1.0); nothing)');
|
||
|
|
|
||
|
|
$this->assertEquals('age >= 18', $result['conditionExpr']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testAgeLessThan()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(age < 65; result_set(1.0); nothing)');
|
||
|
|
|
||
|
|
$this->assertEquals('age < 65', $result['conditionExpr']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testAgeRange()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(age >= 18 && age <= 65; result_set(1.0); nothing)');
|
||
|
|
|
||
|
|
$this->assertStringContainsString('age >= 18', $result['conditionExpr']);
|
||
|
|
$this->assertStringContainsString('age <= 65', $result['conditionExpr']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testRequestedCondition()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('if(requested("GLU"); test_insert("HBA1C"); nothing)');
|
||
|
|
|
||
|
|
$this->assertStringContainsString('requested', $result['conditionExpr']);
|
||
|
|
$this->assertStringContainsString('GLU', $result['conditionExpr']);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// MULTI-RULE TESTS
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
public function testParseMultiRule()
|
||
|
|
{
|
||
|
|
$expr = 'if(sex("M"); result_set(0.5); nothing), if(age > 65; result_set(1.0); nothing)';
|
||
|
|
$rules = $this->service->parseMultiRule($expr);
|
||
|
|
|
||
|
|
$this->assertCount(2, $rules);
|
||
|
|
$this->assertStringContainsString('sex("M")', $rules[0]);
|
||
|
|
$this->assertStringContainsString('age > 65', $rules[1]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testParseMultiRuleThreeRules()
|
||
|
|
{
|
||
|
|
$expr = 'if(sex("M"); result_set(0.5); nothing), if(age > 65; result_set(1.0); nothing), if(priority("S"); result_set(2.0); nothing)';
|
||
|
|
$rules = $this->service->parseMultiRule($expr);
|
||
|
|
|
||
|
|
$this->assertCount(3, $rules);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// ERROR HANDLING TESTS
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
public function testInvalidSyntaxThrowsException()
|
||
|
|
{
|
||
|
|
$this->expectException(\InvalidArgumentException::class);
|
||
|
|
$this->service->compile('invalid syntax here');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testUnknownActionThrowsException()
|
||
|
|
{
|
||
|
|
$this->expectException(\InvalidArgumentException::class);
|
||
|
|
$this->service->compile('if(sex("M"); unknown_action(); nothing)');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testEmptyExpressionReturnsEmptyArray()
|
||
|
|
{
|
||
|
|
$result = $this->service->compile('');
|
||
|
|
|
||
|
|
$this->assertEmpty($result);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// DOCUMENTATION EXAMPLES
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
public function testExample1SexBasedResult()
|
||
|
|
{
|
||
|
|
// Example 1 from docs
|
||
|
|
$result = $this->service->compile('if(sex("M"); result_set(0.5); result_set(0.6))');
|
||
|
|
|
||
|
|
$this->assertEquals('RESULT_SET', $result['then'][0]['type']);
|
||
|
|
$this->assertEquals(0.5, $result['then'][0]['value']);
|
||
|
|
$this->assertEquals('RESULT_SET', $result['else'][0]['type']);
|
||
|
|
$this->assertEquals(0.6, $result['else'][0]['value']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testExample2ConditionalTestInsertion()
|
||
|
|
{
|
||
|
|
// Example 2 from docs
|
||
|
|
$result = $this->service->compile("if(requested('GLU'); test_insert('HBA1C'):test_insert('INS'); nothing)");
|
||
|
|
|
||
|
|
$this->assertCount(2, $result['then']);
|
||
|
|
$this->assertEquals('TEST_INSERT', $result['then'][0]['type']);
|
||
|
|
$this->assertEquals('HBA1C', $result['then'][0]['testCode']);
|
||
|
|
$this->assertEquals('INS', $result['then'][1]['testCode']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testExample3MultipleConditionsAnd()
|
||
|
|
{
|
||
|
|
// Example 3 from docs
|
||
|
|
$result = $this->service->compile("if(sex('M') && age > 40; result_set(1.2); result_set(1.0))");
|
||
|
|
|
||
|
|
$this->assertStringContainsString('and', strtolower($result['conditionExpr']));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testExample4OrCondition()
|
||
|
|
{
|
||
|
|
// Example 4 from docs
|
||
|
|
$result = $this->service->compile("if(sex('M') || age > 65; result_set(1.0); result_set(0.8))");
|
||
|
|
|
||
|
|
$this->assertStringContainsString('or', strtolower($result['conditionExpr']));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testExample5CombinedAndOr()
|
||
|
|
{
|
||
|
|
// Example 5 from docs
|
||
|
|
$result = $this->service->compile("if((sex('M') && age > 40) || (sex('F') && age > 50); result_set(1.5); result_set(1.0))");
|
||
|
|
|
||
|
|
$this->assertStringContainsString('or', strtolower($result['conditionExpr']));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testExample6MultipleActions()
|
||
|
|
{
|
||
|
|
// Example 6 from docs
|
||
|
|
$result = $this->service->compile("if(priority('S'); result_set('URGENT'):test_insert('STAT_TEST'); result_set('NORMAL'))");
|
||
|
|
|
||
|
|
$this->assertCount(2, $result['then']);
|
||
|
|
$this->assertEquals('RESULT_SET', $result['then'][0]['type']);
|
||
|
|
$this->assertEquals('TEST_INSERT', $result['then'][1]['type']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testExample7ThreeActions()
|
||
|
|
{
|
||
|
|
// Example 7 from docs
|
||
|
|
$result = $this->service->compile("if(sex('M') && age > 40; result_set(1.5):test_insert('EXTRA_TEST'):comment_insert('Male over 40'); nothing)");
|
||
|
|
|
||
|
|
$this->assertCount(3, $result['then']);
|
||
|
|
$this->assertEquals('RESULT_SET', $result['then'][0]['type']);
|
||
|
|
$this->assertEquals('TEST_INSERT', $result['then'][1]['type']);
|
||
|
|
$this->assertEquals('COMMENT_INSERT', $result['then'][2]['type']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testExample8ComplexRule()
|
||
|
|
{
|
||
|
|
// Example 8 from docs
|
||
|
|
$result = $this->service->compile("if(sex('F') && (age >= 18 && age <= 50) && priority('S'); result_set('HIGH_PRIO'):comment_insert('Female stat 18-50'); result_set('NORMAL'))");
|
||
|
|
|
||
|
|
$this->assertCount(2, $result['then']);
|
||
|
|
$this->assertStringContainsString('and', strtolower($result['conditionExpr']));
|
||
|
|
}
|
||
|
|
}
|